Commit 839085edadad49a65590d095bfb03cec2fea6abd

Authored by 芋道源码
Committed by Gitee
2 parents a64df8e2 a1b07adb

!1415 AI 大模型最新特性

Merge pull request !1415 from 芋道源码/feature/ai
Showing 67 changed files with 2135 additions and 271 deletions
yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/mq/rabbitmq/TenantRabbitMQInitializer.java
... ... @@ -12,8 +12,10 @@ import org.springframework.beans.factory.config.BeanPostProcessor;
12 12 public class TenantRabbitMQInitializer implements BeanPostProcessor {
13 13  
14 14 @Override
  15 + @SuppressWarnings("PatternVariableCanBeUsed")
15 16 public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
16   - if (bean instanceof RabbitTemplate rabbitTemplate) {
  17 + if (bean instanceof RabbitTemplate) {
  18 + RabbitTemplate rabbitTemplate = (RabbitTemplate) bean;
17 19 rabbitTemplate.addBeforePublishPostProcessors(new TenantRabbitMQMessagePostProcessor());
18 20 }
19 21 return bean;
... ...
yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/mq/rocketmq/TenantRocketMQInitializer.java
... ... @@ -17,10 +17,13 @@ import org.springframework.beans.factory.config.BeanPostProcessor;
17 17 public class TenantRocketMQInitializer implements BeanPostProcessor {
18 18  
19 19 @Override
  20 + @SuppressWarnings("PatternVariableCanBeUsed")
20 21 public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
21   - if (bean instanceof DefaultRocketMQListenerContainer container) {
  22 + if (bean instanceof DefaultRocketMQListenerContainer) {
  23 + DefaultRocketMQListenerContainer container = (DefaultRocketMQListenerContainer) bean;
22 24 initTenantConsumer(container.getConsumer());
23   - } else if (bean instanceof RocketMQTemplate template) {
  25 + } else if (bean instanceof RocketMQTemplate) {
  26 + RocketMQTemplate template = (RocketMQTemplate) bean;
24 27 initTenantProducer(template.getProducer());
25 28 }
26 29 return bean;
... ...
yudao-framework/yudao-spring-boot-starter-job/src/main/java/cn/iocoder/yudao/framework/quartz/config/YudaoAsyncAutoConfiguration.java
... ... @@ -21,15 +21,18 @@ public class YudaoAsyncAutoConfiguration {
21 21 return new BeanPostProcessor() {
22 22  
23 23 @Override
  24 + @SuppressWarnings("PatternVariableCanBeUsed")
24 25 public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
25 26 // 处理 ThreadPoolTaskExecutor
26   - if (bean instanceof ThreadPoolTaskExecutor executor) {
  27 + if (bean instanceof ThreadPoolTaskExecutor) {
  28 + ThreadPoolTaskExecutor executor = (ThreadPoolTaskExecutor) bean;
27 29 executor.setTaskDecorator(TtlRunnable::get);
28 30 return executor;
29 31 }
30 32 // 处理 SimpleAsyncTaskExecutor
31 33 // 参考 https://t.zsxq.com/CBoks 增加
32   - if (bean instanceof SimpleAsyncTaskExecutor executor) {
  34 + if (bean instanceof SimpleAsyncTaskExecutor) {
  35 + SimpleAsyncTaskExecutor executor = (SimpleAsyncTaskExecutor) bean;
33 36 executor.setTaskDecorator(TtlRunnable::get);
34 37 return executor;
35 38 }
... ...
yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/config/YudaoMybatisAutoConfiguration.java
... ... @@ -10,7 +10,6 @@ import com.baomidou.mybatisplus.extension.incrementer.*;
10 10 import com.baomidou.mybatisplus.extension.parser.JsqlParserGlobal;
11 11 import com.baomidou.mybatisplus.extension.parser.cache.JdkSerialCaffeineJsqlParseCache;
12 12 import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
13   -import com.baomidou.mybatisplus.extension.plugins.inner.BlockAttackInnerInterceptor;
14 13 import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
15 14 import org.apache.ibatis.annotations.Mapper;
16 15 import org.mybatis.spring.annotation.MapperScan;
... ... @@ -43,7 +42,8 @@ public class YudaoMybatisAutoConfiguration {
43 42 public MybatisPlusInterceptor mybatisPlusInterceptor() {
44 43 MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor();
45 44 mybatisPlusInterceptor.addInnerInterceptor(new PaginationInnerInterceptor()); // 分页插件
46   - mybatisPlusInterceptor.addInnerInterceptor(new BlockAttackInnerInterceptor()); // 拦截没有指定条件的 update 和 delete 语句
  45 + // ↓↓↓ 按需开启,可能会影响到 updateBatch 的地方:例如说文件配置管理 ↓↓↓
  46 + // mybatisPlusInterceptor.addInnerInterceptor(new BlockAttackInnerInterceptor()); // 拦截没有指定条件的 update 和 delete 语句
47 47 return mybatisPlusInterceptor;
48 48 }
49 49  
... ...
yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/handler/DefaultDBFieldHandler.java
... ... @@ -18,8 +18,10 @@ import java.util.Objects;
18 18 public class DefaultDBFieldHandler implements MetaObjectHandler {
19 19  
20 20 @Override
  21 + @SuppressWarnings("PatternVariableCanBeUsed")
21 22 public void insertFill(MetaObject metaObject) {
22   - if (Objects.nonNull(metaObject) && metaObject.getOriginalObject() instanceof BaseDO baseDO) {
  23 + if (Objects.nonNull(metaObject) && metaObject.getOriginalObject() instanceof BaseDO) {
  24 + BaseDO baseDO = (BaseDO) metaObject.getOriginalObject();
23 25  
24 26 LocalDateTime current = LocalDateTime.now();
25 27 // 创建时间为空,则以当前时间为插入时间
... ...
yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/handler/GlobalExceptionHandler.java
... ... @@ -182,9 +182,11 @@ public class GlobalExceptionHandler {
182 182 * 例如说,接口上设置了 @RequestBody 实体中 xx 属性类型为 Integer,结果传递 xx 参数类型为 String
183 183 */
184 184 @ExceptionHandler(HttpMessageNotReadableException.class)
  185 + @SuppressWarnings("PatternVariableCanBeUsed")
185 186 public CommonResult<?> methodArgumentTypeInvalidFormatExceptionHandler(HttpMessageNotReadableException ex) {
186 187 log.warn("[methodArgumentTypeInvalidFormatExceptionHandler]", ex);
187   - if (ex.getCause() instanceof InvalidFormatException invalidFormatException) {
  188 + if (ex.getCause() instanceof InvalidFormatException) {
  189 + InvalidFormatException invalidFormatException = (InvalidFormatException) ex.getCause();
188 190 return CommonResult.error(BAD_REQUEST.getCode(), String.format("请求参数类型错误:%s", invalidFormatException.getValue()));
189 191 }
190 192 if (StrUtil.startWith(ex.getMessage(), "Required request body is missing")) {
... ...
yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/util/WebFrameworkUtils.java
... ... @@ -144,11 +144,13 @@ public class WebFrameworkUtils {
144 144 return (CommonResult<?>) request.getAttribute(REQUEST_ATTRIBUTE_COMMON_RESULT);
145 145 }
146 146  
  147 + @SuppressWarnings("PatternVariableCanBeUsed")
147 148 public static HttpServletRequest getRequest() {
148 149 RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
149   - if (!(requestAttributes instanceof ServletRequestAttributes servletRequestAttributes)) {
  150 + if (!(requestAttributes instanceof ServletRequestAttributes)) {
150 151 return null;
151 152 }
  153 + ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) requestAttributes;
152 154 return servletRequestAttributes.getRequest();
153 155 }
154 156  
... ...
yudao-module-ai/pom.xml
... ... @@ -19,8 +19,8 @@
19 19 国外:OpenAI、Ollama、Midjourney、StableDiffusion、Suno
20 20 </description>
21 21 <properties>
22   - <spring-ai.version>1.0.0</spring-ai.version>
23   - <alibaba-ai.version>1.0.0.2</alibaba-ai.version>
  22 + <spring-ai.version>1.0.1</spring-ai.version>
  23 + <alibaba-ai.version>1.0.0.3</alibaba-ai.version>
24 24 <tinyflow.version>1.0.2</tinyflow.version>
25 25 </properties>
26 26  
... ... @@ -86,6 +86,11 @@
86 86 </dependency>
87 87 <dependency>
88 88 <groupId>org.springframework.ai</groupId>
  89 + <artifactId>spring-ai-starter-model-anthropic</artifactId>
  90 + <version>${spring-ai.version}</version>
  91 + </dependency>
  92 + <dependency>
  93 + <groupId>org.springframework.ai</groupId>
89 94 <artifactId>spring-ai-starter-model-deepseek</artifactId>
90 95 <version>${spring-ai.version}</version>
91 96 </dependency>
... ... @@ -182,6 +187,24 @@
182 187 </exclusions>
183 188 </dependency>
184 189  
  190 + <!-- MCP 相关 -->
  191 + <!--
  192 + 特殊说明:不能使用 spring-ai-starter-mcp-server-webflux 或 spring-ai-starter-mcp-client-webflux !!!
  193 + 原因:项目使用了 SpringMVC,而不是 WebFlux。引入上述 2 个,会导致 SSE Server 失效。
  194 + -->
  195 + <dependency>
  196 + <!-- 服务端 -->
  197 + <groupId>org.springframework.ai</groupId>
  198 + <artifactId>spring-ai-starter-mcp-server-webmvc</artifactId>
  199 + <version>${spring-ai.version}</version>
  200 + </dependency>
  201 + <dependency>
  202 + <!-- 客户端 -->
  203 + <groupId>org.springframework.ai</groupId>
  204 + <artifactId>spring-ai-starter-mcp-client</artifactId>
  205 + <version>${spring-ai.version}</version>
  206 + </dependency>
  207 +
185 208 <!-- TinyFlow:AI 工作流 -->
186 209 <dependency>
187 210 <groupId>dev.tinyflow</groupId>
... ...
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/AiChatMessageController.http
... ... @@ -20,9 +20,46 @@ tenant-id: {{adminTenantId}}
20 20 "content": "1+1=?"
21 21 }
22 22  
  23 +### 发送消息(流式)【带文件】
  24 +POST {{baseUrl}}/ai/chat/message/send-stream
  25 +Content-Type: application/json
  26 +Authorization: {{token}}
  27 +tenant-id: {{adminTenantId}}
  28 +
  29 +{
  30 + "conversationId": "1781604279872581797",
  31 + "content": "图片里有什么?",
  32 + "attachmentUrls": ["http://test.yudao.iocoder.cn/1755531278.jpeg"]
  33 +}
  34 +
  35 +### 发送消息(流式)【追问带文件】
  36 +POST {{baseUrl}}/ai/chat/message/send-stream
  37 +Content-Type: application/json
  38 +Authorization: {{token}}
  39 +tenant-id: {{adminTenantId}}
  40 +
  41 +{
  42 + "conversationId": "1781604279872581799",
  43 + "content": "说下图片里,有哪些字?",
  44 + "useContext": true
  45 +}
  46 +
  47 +### 发送消息(流式)【联网搜索】
  48 +POST {{baseUrl}}/ai/chat/message/send-stream
  49 +Content-Type: application/json
  50 +Authorization: {{token}}
  51 +tenant-id: {{adminTenantId}}
  52 +
  53 +{
  54 + "conversationId": "1781604279872581799",
  55 + "content": "今天是周几?",
  56 + "useSearch": true
  57 +}
  58 +
23 59 ### 获得指定对话的消息列表
24   -GET {{baseUrl}}/ai/chat/message/list-by-conversation-id?conversationId=1781604279872581649
  60 +GET {{baseUrl}}/ai/chat/message/list-by-conversation-id?conversationId=1781604279872581799
25 61 Authorization: {{token}}
  62 +tenant-id: {{adminTenantId}}
26 63  
27 64 ### 删除消息
28 65 DELETE {{baseUrl}}/ai/chat/message/delete?id=50
... ...
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/vo/message/AiChatMessageRespVO.java
1 1 package cn.iocoder.yudao.module.ai.controller.admin.chat.vo.message;
2 2  
  3 +import cn.iocoder.yudao.module.ai.framework.ai.core.webserch.AiWebSearchResponse;
3 4 import io.swagger.v3.oas.annotations.media.Schema;
4 5 import lombok.Data;
5 6  
... ... @@ -37,6 +38,9 @@ public class AiChatMessageRespVO {
37 38 @Schema(description = "聊天内容", requiredMode = Schema.RequiredMode.REQUIRED, example = "你好,你好啊")
38 39 private String content;
39 40  
  41 + @Schema(description = "推理内容", example = "要达到这个目标,你需要...")
  42 + private String reasoningContent;
  43 +
40 44 @Schema(description = "是否携带上下文", requiredMode = Schema.RequiredMode.REQUIRED, example = "true")
41 45 private Boolean useContext;
42 46  
... ... @@ -46,6 +50,12 @@ public class AiChatMessageRespVO {
46 50 @Schema(description = "知识库段落数组")
47 51 private List<KnowledgeSegment> segments;
48 52  
  53 + @Schema(description = "联网搜索的网页内容数组")
  54 + private List<AiWebSearchResponse.WebPage> webSearchPages;
  55 +
  56 + @Schema(description = "附件 URL 数组", example = "https://www.iocoder.cn/1.png")
  57 + private List<String> attachmentUrls;
  58 +
49 59 @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED, example = "2024-05-12 12:51")
50 60 private LocalDateTime createTime;
51 61  
... ...
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/vo/message/AiChatMessageSendReqVO.java
... ... @@ -3,9 +3,9 @@ package cn.iocoder.yudao.module.ai.controller.admin.chat.vo.message;
3 3 import io.swagger.v3.oas.annotations.media.Schema;
4 4 import jakarta.validation.constraints.NotEmpty;
5 5 import jakarta.validation.constraints.NotNull;
6   -import jakarta.validation.constraints.Size;
7 6 import lombok.Data;
8   -import lombok.experimental.Accessors;
  7 +
  8 +import java.util.List;
9 9  
10 10 @Schema(description = "管理后台 - AI 聊天消息发送 Request VO")
11 11 @Data
... ... @@ -22,4 +22,10 @@ public class AiChatMessageSendReqVO {
22 22 @Schema(description = "是否携带上下文", example = "true")
23 23 private Boolean useContext;
24 24  
  25 + @Schema(description = "是否联网搜索", example = "true")
  26 + private Boolean useSearch;
  27 +
  28 + @Schema(description = "附件 URL 数组", example = "https://www.iocoder.cn/1.png")
  29 + private List<String> attachmentUrls;
  30 +
25 31 }
... ...
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/vo/message/AiChatMessageSendRespVO.java
1 1 package cn.iocoder.yudao.module.ai.controller.admin.chat.vo.message;
2 2  
  3 +import cn.iocoder.yudao.module.ai.framework.ai.core.webserch.AiWebSearchResponse;
3 4 import io.swagger.v3.oas.annotations.media.Schema;
4 5 import lombok.Data;
5 6  
... ... @@ -29,12 +30,18 @@ public class AiChatMessageSendRespVO {
29 30 @Schema(description = "聊天内容", requiredMode = Schema.RequiredMode.REQUIRED, example = "你好,你好啊")
30 31 private String content;
31 32  
  33 + @Schema(description = "推理内容", example = "要达到这个目标,你需要...")
  34 + private String reasoningContent;
  35 +
32 36 @Schema(description = "知识库段落编号数组", example = "[1,2,3]")
33 37 private List<Long> segmentIds;
34 38  
35 39 @Schema(description = "知识库段落数组")
36 40 private List<AiChatMessageRespVO.KnowledgeSegment> segments;
37 41  
  42 + @Schema(description = "联网搜索的网页内容数组")
  43 + private List<AiWebSearchResponse.WebPage> webSearchPages;
  44 +
38 45 @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
39 46 private LocalDateTime createTime;
40 47  
... ...
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/chatRole/AiChatRoleRespVO.java
... ... @@ -52,6 +52,9 @@ public class AiChatRoleRespVO implements VO {
52 52 @Schema(description = "引用的工具编号列表", example = "1,2,3")
53 53 private List<Long> toolIds;
54 54  
  55 + @Schema(description = "引用的 MCP Client 名字列表", example = "filesystem")
  56 + private List<String> mcpClientNames;
  57 +
55 58 @Schema(description = "是否公开", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
56 59 private Boolean publicStatus;
57 60  
... ...
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/chatRole/AiChatRoleSaveMyReqVO.java
... ... @@ -37,4 +37,7 @@ public class AiChatRoleSaveMyReqVO {
37 37 @Schema(description = "引用的工具编号列表", example = "1,2,3")
38 38 private List<Long> toolIds;
39 39  
  40 + @Schema(description = "引用的 MCP Client 名字列表", example = "filesystem")
  41 + private List<String> mcpClientNames;
  42 +
40 43 }
41 44 \ No newline at end of file
... ...
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/chatRole/AiChatRoleSaveReqVO.java
... ... @@ -50,6 +50,9 @@ public class AiChatRoleSaveReqVO {
50 50 @Schema(description = "引用的工具编号列表", example = "1,2,3")
51 51 private List<Long> toolIds;
52 52  
  53 + @Schema(description = "引用的 MCP Client 名字列表", example = "filesystem")
  54 + private List<String> mcpClientNames;
  55 +
53 56 @Schema(description = "是否公开", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
54 57 @NotNull(message = "是否公开不能为空")
55 58 private Boolean publicStatus;
... ...
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/chat/AiChatMessageDO.java
... ... @@ -2,14 +2,20 @@ package cn.iocoder.yudao.module.ai.dal.dataobject.chat;
2 2  
3 3 import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
4 4 import cn.iocoder.yudao.framework.mybatis.core.type.LongListTypeHandler;
  5 +import cn.iocoder.yudao.framework.mybatis.core.type.StringListTypeHandler;
5 6 import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeSegmentDO;
6 7 import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatRoleDO;
7 8 import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiModelDO;
  9 +import cn.iocoder.yudao.module.ai.framework.ai.core.webserch.AiWebSearchResponse;
8 10 import com.baomidou.mybatisplus.annotation.KeySequence;
9 11 import com.baomidou.mybatisplus.annotation.TableField;
10 12 import com.baomidou.mybatisplus.annotation.TableId;
11 13 import com.baomidou.mybatisplus.annotation.TableName;
12   -import lombok.*;
  14 +import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
  15 +import lombok.AllArgsConstructor;
  16 +import lombok.Builder;
  17 +import lombok.Data;
  18 +import lombok.NoArgsConstructor;
13 19 import org.springframework.ai.chat.messages.MessageType;
14 20  
15 21 import java.util.List;
... ... @@ -87,6 +93,10 @@ public class AiChatMessageDO extends BaseDO {
87 93 * 聊天内容
88 94 */
89 95 private String content;
  96 + /**
  97 + * 推理内容
  98 + */
  99 + private String reasoningContent;
90 100  
91 101 /**
92 102 * 是否携带上下文
... ... @@ -101,4 +111,16 @@ public class AiChatMessageDO extends BaseDO {
101 111 @TableField(typeHandler = LongListTypeHandler.class)
102 112 private List<Long> segmentIds;
103 113  
  114 + /**
  115 + * 联网搜索的网页内容数组
  116 + */
  117 + @TableField(typeHandler = JacksonTypeHandler.class)
  118 + private List<AiWebSearchResponse.WebPage> webSearchPages;
  119 +
  120 + /**
  121 + * 附件 URL 数组
  122 + */
  123 + @TableField(typeHandler = StringListTypeHandler.class)
  124 + private List<String> attachmentUrls;
  125 +
104 126 }
... ...
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/model/AiChatRoleDO.java
... ... @@ -3,6 +3,7 @@ package cn.iocoder.yudao.module.ai.dal.dataobject.model;
3 3 import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
4 4 import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
5 5 import cn.iocoder.yudao.framework.mybatis.core.type.LongListTypeHandler;
  6 +import cn.iocoder.yudao.framework.mybatis.core.type.StringListTypeHandler;
6 7 import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeDO;
7 8 import com.baomidou.mybatisplus.annotation.KeySequence;
8 9 import com.baomidou.mybatisplus.annotation.TableField;
... ... @@ -80,6 +81,13 @@ public class AiChatRoleDO extends BaseDO {
80 81 */
81 82 @TableField(typeHandler = LongListTypeHandler.class)
82 83 private List<Long> toolIds;
  84 + /**
  85 + * 引用的 MCP Client 名字列表
  86 + *
  87 + * 关联 spring.ai.mcp.client 下的名字
  88 + */
  89 + @TableField(typeHandler = StringListTypeHandler.class)
  90 + private List<String> mcpClientNames;
83 91  
84 92 /**
85 93 * 是否公开
... ...
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/model/AiToolDO.java
1 1 package cn.iocoder.yudao.module.ai.dal.dataobject.model;
2 2  
3 3 import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
4   -import cn.iocoder.yudao.module.ai.service.model.tool.DirectoryListToolFunction;
5   -import cn.iocoder.yudao.module.ai.service.model.tool.WeatherQueryToolFunction;
  4 +import cn.iocoder.yudao.module.ai.tool.function.DirectoryListToolFunction;
  5 +import cn.iocoder.yudao.module.ai.tool.function.WeatherQueryToolFunction;
6 6 import com.baomidou.mybatisplus.annotation.KeySequence;
7 7 import com.baomidou.mybatisplus.annotation.TableId;
8 8 import com.baomidou.mybatisplus.annotation.TableName;
... ...
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/enums/model/AiPlatformEnum.java
... ... @@ -33,6 +33,8 @@ public enum AiPlatformEnum implements ArrayValuable&lt;String&gt; {
33 33  
34 34 OPENAI("OpenAI", "OpenAI"), // OpenAI 官方
35 35 AZURE_OPENAI("AzureOpenAI", "AzureOpenAI"), // OpenAI 微软
  36 + ANTHROPIC("Anthropic", "Anthropic"), // Anthropic Claude
  37 + GEMINI("Gemini", "Gemini"), // 谷歌 Gemini
36 38 OLLAMA("Ollama", "Ollama"),
37 39  
38 40 STABLE_DIFFUSION("StableDiffusion", "StableDiffusion"), // Stability AI
... ...
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/config/AiAutoConfiguration.java
... ... @@ -2,25 +2,34 @@ package cn.iocoder.yudao.module.ai.framework.ai.config;
2 2  
3 3 import cn.hutool.core.util.StrUtil;
4 4 import cn.hutool.extra.spring.SpringUtil;
5   -import cn.iocoder.yudao.module.ai.framework.ai.core.AiModelFactory;
6   -import cn.iocoder.yudao.module.ai.framework.ai.core.AiModelFactoryImpl;
  5 +import cn.iocoder.yudao.module.ai.framework.ai.core.model.AiModelFactory;
  6 +import cn.iocoder.yudao.module.ai.framework.ai.core.model.AiModelFactoryImpl;
7 7 import cn.iocoder.yudao.module.ai.framework.ai.core.model.baichuan.BaiChuanChatModel;
8 8 import cn.iocoder.yudao.module.ai.framework.ai.core.model.doubao.DouBaoChatModel;
  9 +import cn.iocoder.yudao.module.ai.framework.ai.core.model.gemini.GeminiChatModel;
9 10 import cn.iocoder.yudao.module.ai.framework.ai.core.model.hunyuan.HunYuanChatModel;
10 11 import cn.iocoder.yudao.module.ai.framework.ai.core.model.midjourney.api.MidjourneyApi;
11 12 import cn.iocoder.yudao.module.ai.framework.ai.core.model.siliconflow.SiliconFlowApiConstants;
12 13 import cn.iocoder.yudao.module.ai.framework.ai.core.model.siliconflow.SiliconFlowChatModel;
13 14 import cn.iocoder.yudao.module.ai.framework.ai.core.model.suno.api.SunoApi;
14 15 import cn.iocoder.yudao.module.ai.framework.ai.core.model.xinghuo.XingHuoChatModel;
  16 +import cn.iocoder.yudao.module.ai.framework.ai.core.webserch.AiWebSearchClient;
  17 +import cn.iocoder.yudao.module.ai.framework.ai.core.webserch.bocha.AiBoChaWebSearchClient;
  18 +import cn.iocoder.yudao.module.ai.tool.method.PersonService;
15 19 import lombok.extern.slf4j.Slf4j;
  20 +import org.springframework.ai.deepseek.DeepSeekChatModel;
  21 +import org.springframework.ai.deepseek.DeepSeekChatOptions;
  22 +import org.springframework.ai.deepseek.api.DeepSeekApi;
16 23 import org.springframework.ai.embedding.BatchingStrategy;
17 24 import org.springframework.ai.embedding.TokenCountBatchingStrategy;
18 25 import org.springframework.ai.model.tool.ToolCallingManager;
19 26 import org.springframework.ai.openai.OpenAiChatModel;
20 27 import org.springframework.ai.openai.OpenAiChatOptions;
21 28 import org.springframework.ai.openai.api.OpenAiApi;
  29 +import org.springframework.ai.support.ToolCallbacks;
22 30 import org.springframework.ai.tokenizer.JTokkitTokenCountEstimator;
23 31 import org.springframework.ai.tokenizer.TokenCountEstimator;
  32 +import org.springframework.ai.tool.ToolCallback;
24 33 import org.springframework.ai.vectorstore.milvus.autoconfigure.MilvusServiceClientProperties;
25 34 import org.springframework.ai.vectorstore.milvus.autoconfigure.MilvusVectorStoreProperties;
26 35 import org.springframework.ai.vectorstore.qdrant.autoconfigure.QdrantVectorStoreProperties;
... ... @@ -30,6 +39,8 @@ import org.springframework.boot.context.properties.EnableConfigurationProperties
30 39 import org.springframework.context.annotation.Bean;
31 40 import org.springframework.context.annotation.Configuration;
32 41  
  42 +import java.util.List;
  43 +
33 44 /**
34 45 * 芋道 AI 自动配置
35 46 *
... ... @@ -52,19 +63,48 @@ public class AiAutoConfiguration {
52 63 // ========== 各种 AI Client 创建 ==========
53 64  
54 65 @Bean
  66 + @ConditionalOnProperty(value = "yudao.ai.gemini.enable", havingValue = "true")
  67 + public GeminiChatModel geminiChatModel(YudaoAiProperties yudaoAiProperties) {
  68 + YudaoAiProperties.Gemini properties = yudaoAiProperties.getGemini();
  69 + return buildGeminiChatClient(properties);
  70 + }
  71 +
  72 + public GeminiChatModel buildGeminiChatClient(YudaoAiProperties.Gemini properties) {
  73 + if (StrUtil.isEmpty(properties.getModel())) {
  74 + properties.setModel(GeminiChatModel.MODEL_DEFAULT);
  75 + }
  76 + OpenAiChatModel openAiChatModel = OpenAiChatModel.builder()
  77 + .openAiApi(OpenAiApi.builder()
  78 + .baseUrl(GeminiChatModel.BASE_URL)
  79 + .completionsPath(GeminiChatModel.COMPLETE_PATH)
  80 + .apiKey(properties.getApiKey())
  81 + .build())
  82 + .defaultOptions(OpenAiChatOptions.builder()
  83 + .model(properties.getModel())
  84 + .temperature(properties.getTemperature())
  85 + .maxTokens(properties.getMaxTokens())
  86 + .topP(properties.getTopP())
  87 + .build())
  88 + .toolCallingManager(getToolCallingManager())
  89 + .build();
  90 + return new GeminiChatModel(openAiChatModel);
  91 + }
  92 +
  93 + @Bean
55 94 @ConditionalOnProperty(value = "yudao.ai.doubao.enable", havingValue = "true")
56 95 public DouBaoChatModel douBaoChatClient(YudaoAiProperties yudaoAiProperties) {
57   - YudaoAiProperties.DouBaoProperties properties = yudaoAiProperties.getDoubao();
  96 + YudaoAiProperties.DouBao properties = yudaoAiProperties.getDoubao();
58 97 return buildDouBaoChatClient(properties);
59 98 }
60 99  
61   - public DouBaoChatModel buildDouBaoChatClient(YudaoAiProperties.DouBaoProperties properties) {
  100 + public DouBaoChatModel buildDouBaoChatClient(YudaoAiProperties.DouBao properties) {
62 101 if (StrUtil.isEmpty(properties.getModel())) {
63 102 properties.setModel(DouBaoChatModel.MODEL_DEFAULT);
64 103 }
65 104 OpenAiChatModel openAiChatModel = OpenAiChatModel.builder()
66 105 .openAiApi(OpenAiApi.builder()
67 106 .baseUrl(DouBaoChatModel.BASE_URL)
  107 + .completionsPath(DouBaoChatModel.COMPLETE_PATH)
68 108 .apiKey(properties.getApiKey())
69 109 .build())
70 110 .defaultOptions(OpenAiChatOptions.builder()
... ... @@ -81,20 +121,20 @@ public class AiAutoConfiguration {
81 121 @Bean
82 122 @ConditionalOnProperty(value = "yudao.ai.siliconflow.enable", havingValue = "true")
83 123 public SiliconFlowChatModel siliconFlowChatClient(YudaoAiProperties yudaoAiProperties) {
84   - YudaoAiProperties.SiliconFlowProperties properties = yudaoAiProperties.getSiliconflow();
  124 + YudaoAiProperties.SiliconFlow properties = yudaoAiProperties.getSiliconflow();
85 125 return buildSiliconFlowChatClient(properties);
86 126 }
87 127  
88   - public SiliconFlowChatModel buildSiliconFlowChatClient(YudaoAiProperties.SiliconFlowProperties properties) {
  128 + public SiliconFlowChatModel buildSiliconFlowChatClient(YudaoAiProperties.SiliconFlow properties) {
89 129 if (StrUtil.isEmpty(properties.getModel())) {
90 130 properties.setModel(SiliconFlowApiConstants.MODEL_DEFAULT);
91 131 }
92   - OpenAiChatModel openAiChatModel = OpenAiChatModel.builder()
93   - .openAiApi(OpenAiApi.builder()
  132 + DeepSeekChatModel openAiChatModel = DeepSeekChatModel.builder()
  133 + .deepSeekApi(DeepSeekApi.builder()
94 134 .baseUrl(SiliconFlowApiConstants.DEFAULT_BASE_URL)
95 135 .apiKey(properties.getApiKey())
96 136 .build())
97   - .defaultOptions(OpenAiChatOptions.builder()
  137 + .defaultOptions(DeepSeekChatOptions.builder()
98 138 .model(properties.getModel())
99 139 .temperature(properties.getTemperature())
100 140 .maxTokens(properties.getMaxTokens())
... ... @@ -108,11 +148,11 @@ public class AiAutoConfiguration {
108 148 @Bean
109 149 @ConditionalOnProperty(value = "yudao.ai.hunyuan.enable", havingValue = "true")
110 150 public HunYuanChatModel hunYuanChatClient(YudaoAiProperties yudaoAiProperties) {
111   - YudaoAiProperties.HunYuanProperties properties = yudaoAiProperties.getHunyuan();
  151 + YudaoAiProperties.HunYuan properties = yudaoAiProperties.getHunyuan();
112 152 return buildHunYuanChatClient(properties);
113 153 }
114 154  
115   - public HunYuanChatModel buildHunYuanChatClient(YudaoAiProperties.HunYuanProperties properties) {
  155 + public HunYuanChatModel buildHunYuanChatClient(YudaoAiProperties.HunYuan properties) {
116 156 if (StrUtil.isEmpty(properties.getModel())) {
117 157 properties.setModel(HunYuanChatModel.MODEL_DEFAULT);
118 158 }
... ... @@ -122,13 +162,14 @@ public class AiAutoConfiguration {
122 162 StrUtil.startWithIgnoreCase(properties.getModel(), "deepseek") ? HunYuanChatModel.DEEP_SEEK_BASE_URL
123 163 : HunYuanChatModel.BASE_URL);
124 164 }
125   - // 创建 OpenAiChatModel、HunYuanChatModel 对象
126   - OpenAiChatModel openAiChatModel = OpenAiChatModel.builder()
127   - .openAiApi(OpenAiApi.builder()
  165 + // 创建 DeepSeekChatModel、HunYuanChatModel 对象
  166 + DeepSeekChatModel openAiChatModel = DeepSeekChatModel.builder()
  167 + .deepSeekApi(DeepSeekApi.builder()
128 168 .baseUrl(properties.getBaseUrl())
  169 + .completionsPath(HunYuanChatModel.COMPLETE_PATH)
129 170 .apiKey(properties.getApiKey())
130 171 .build())
131   - .defaultOptions(OpenAiChatOptions.builder()
  172 + .defaultOptions(DeepSeekChatOptions.builder()
132 173 .model(properties.getModel())
133 174 .temperature(properties.getTemperature())
134 175 .maxTokens(properties.getMaxTokens())
... ... @@ -142,25 +183,30 @@ public class AiAutoConfiguration {
142 183 @Bean
143 184 @ConditionalOnProperty(value = "yudao.ai.xinghuo.enable", havingValue = "true")
144 185 public XingHuoChatModel xingHuoChatClient(YudaoAiProperties yudaoAiProperties) {
145   - YudaoAiProperties.XingHuoProperties properties = yudaoAiProperties.getXinghuo();
  186 + YudaoAiProperties.XingHuo properties = yudaoAiProperties.getXinghuo();
146 187 return buildXingHuoChatClient(properties);
147 188 }
148 189  
149   - public XingHuoChatModel buildXingHuoChatClient(YudaoAiProperties.XingHuoProperties properties) {
  190 + public XingHuoChatModel buildXingHuoChatClient(YudaoAiProperties.XingHuo properties) {
150 191 if (StrUtil.isEmpty(properties.getModel())) {
151 192 properties.setModel(XingHuoChatModel.MODEL_DEFAULT);
152 193 }
  194 + OpenAiApi.Builder builder = OpenAiApi.builder()
  195 + .baseUrl(XingHuoChatModel.BASE_URL_V1)
  196 + .apiKey(properties.getAppKey() + ":" + properties.getSecretKey());
  197 + if ("x1".equals(properties.getModel())) {
  198 + builder.baseUrl(XingHuoChatModel.BASE_URL_V2)
  199 + .completionsPath(XingHuoChatModel.BASE_COMPLETIONS_PATH_V2);
  200 + }
153 201 OpenAiChatModel openAiChatModel = OpenAiChatModel.builder()
154   - .openAiApi(OpenAiApi.builder()
155   - .baseUrl(XingHuoChatModel.BASE_URL)
156   - .apiKey(properties.getAppKey() + ":" + properties.getSecretKey())
157   - .build())
  202 + .openAiApi(builder.build())
158 203 .defaultOptions(OpenAiChatOptions.builder()
159 204 .model(properties.getModel())
160 205 .temperature(properties.getTemperature())
161 206 .maxTokens(properties.getMaxTokens())
162 207 .topP(properties.getTopP())
163 208 .build())
  209 + // TODO @芋艿:星火的 function call 有 bug,会报 ToolResponseMessage must have an id 错误!!!
164 210 .toolCallingManager(getToolCallingManager())
165 211 .build();
166 212 return new XingHuoChatModel(openAiChatModel);
... ... @@ -169,11 +215,11 @@ public class AiAutoConfiguration {
169 215 @Bean
170 216 @ConditionalOnProperty(value = "yudao.ai.baichuan.enable", havingValue = "true")
171 217 public BaiChuanChatModel baiChuanChatClient(YudaoAiProperties yudaoAiProperties) {
172   - YudaoAiProperties.BaiChuanProperties properties = yudaoAiProperties.getBaichuan();
  218 + YudaoAiProperties.BaiChuan properties = yudaoAiProperties.getBaichuan();
173 219 return buildBaiChuanChatClient(properties);
174 220 }
175 221  
176   - public BaiChuanChatModel buildBaiChuanChatClient(YudaoAiProperties.BaiChuanProperties properties) {
  222 + public BaiChuanChatModel buildBaiChuanChatClient(YudaoAiProperties.BaiChuan properties) {
177 223 if (StrUtil.isEmpty(properties.getModel())) {
178 224 properties.setModel(BaiChuanChatModel.MODEL_DEFAULT);
179 225 }
... ... @@ -196,7 +242,7 @@ public class AiAutoConfiguration {
196 242 @Bean
197 243 @ConditionalOnProperty(value = "yudao.ai.midjourney.enable", havingValue = "true")
198 244 public MidjourneyApi midjourneyApi(YudaoAiProperties yudaoAiProperties) {
199   - YudaoAiProperties.MidjourneyProperties config = yudaoAiProperties.getMidjourney();
  245 + YudaoAiProperties.Midjourney config = yudaoAiProperties.getMidjourney();
200 246 return new MidjourneyApi(config.getBaseUrl(), config.getApiKey(), config.getNotifyUrl());
201 247 }
202 248  
... ... @@ -222,4 +268,22 @@ public class AiAutoConfiguration {
222 268 return SpringUtil.getBean(ToolCallingManager.class);
223 269 }
224 270  
  271 + // ========== Web Search 相关 ==========
  272 +
  273 + @Bean
  274 + @ConditionalOnProperty(value = "yudao.ai.web-search.enable", havingValue = "true")
  275 + public AiWebSearchClient webSearchClient(YudaoAiProperties yudaoAiProperties) {
  276 + return new AiBoChaWebSearchClient(yudaoAiProperties.getWebSearch().getApiKey());
  277 + }
  278 +
  279 + // ========== MCP 相关 ==========
  280 +
  281 + /**
  282 + * 参考自 <a href="https://docs.spring.io/spring-ai/reference/api/mcp/mcp-client-boot-starter-docs.html">MCP Server Boot Starter</>
  283 + */
  284 + @Bean
  285 + public List<ToolCallback> toolCallbacks(PersonService personService) {
  286 + return List.of(ToolCallbacks.from(personService));
  287 + }
  288 +
225 289 }
226 290 \ No newline at end of file
... ...
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/config/YudaoAiProperties.java
... ... @@ -14,48 +14,66 @@ import org.springframework.boot.context.properties.ConfigurationProperties;
14 14 public class YudaoAiProperties {
15 15  
16 16 /**
  17 + * 谷歌 Gemini
  18 + */
  19 + private Gemini gemini;
  20 +
  21 + /**
17 22 * 字节豆包
18 23 */
19   - @SuppressWarnings("SpellCheckingInspection")
20   - private DouBaoProperties doubao;
  24 + private DouBao doubao;
21 25  
22 26 /**
23 27 * 腾讯混元
24 28 */
25   - @SuppressWarnings("SpellCheckingInspection")
26   - private HunYuanProperties hunyuan;
  29 + private HunYuan hunyuan;
27 30  
28 31 /**
29 32 * 硅基流动
30 33 */
31   - @SuppressWarnings("SpellCheckingInspection")
32   - private SiliconFlowProperties siliconflow;
  34 + private SiliconFlow siliconflow;
33 35  
34 36 /**
35 37 * 讯飞星火
36 38 */
37   - @SuppressWarnings("SpellCheckingInspection")
38   - private XingHuoProperties xinghuo;
  39 + private XingHuo xinghuo;
39 40  
40 41 /**
41 42 * 百川
42 43 */
43   - @SuppressWarnings("SpellCheckingInspection")
44   - private BaiChuanProperties baichuan;
  44 + private BaiChuan baichuan;
45 45  
46 46 /**
47 47 * Midjourney 绘图
48 48 */
49   - private MidjourneyProperties midjourney;
  49 + private Midjourney midjourney;
50 50  
51 51 /**
52 52 * Suno 音乐
53 53 */
54 54 @SuppressWarnings("SpellCheckingInspection")
55   - private SunoProperties suno;
  55 + private Suno suno;
  56 +
  57 + /**
  58 + * 网络搜索
  59 + */
  60 + private WebSearch webSearch;
  61 +
  62 + @Data
  63 + public static class Gemini {
  64 +
  65 + private String enable;
  66 + private String apiKey;
  67 +
  68 + private String model;
  69 + private Double temperature;
  70 + private Integer maxTokens;
  71 + private Double topP;
  72 +
  73 + }
56 74  
57 75 @Data
58   - public static class DouBaoProperties {
  76 + public static class DouBao {
59 77  
60 78 private String enable;
61 79 private String apiKey;
... ... @@ -68,7 +86,7 @@ public class YudaoAiProperties {
68 86 }
69 87  
70 88 @Data
71   - public static class HunYuanProperties {
  89 + public static class HunYuan {
72 90  
73 91 private String enable;
74 92 private String baseUrl;
... ... @@ -82,7 +100,7 @@ public class YudaoAiProperties {
82 100 }
83 101  
84 102 @Data
85   - public static class SiliconFlowProperties {
  103 + public static class SiliconFlow {
86 104  
87 105 private String enable;
88 106 private String apiKey;
... ... @@ -95,7 +113,7 @@ public class YudaoAiProperties {
95 113 }
96 114  
97 115 @Data
98   - public static class XingHuoProperties {
  116 + public static class XingHuo {
99 117  
100 118 private String enable;
101 119 private String appId;
... ... @@ -110,7 +128,7 @@ public class YudaoAiProperties {
110 128 }
111 129  
112 130 @Data
113   - public static class BaiChuanProperties {
  131 + public static class BaiChuan {
114 132  
115 133 private String enable;
116 134 private String apiKey;
... ... @@ -123,7 +141,7 @@ public class YudaoAiProperties {
123 141 }
124 142  
125 143 @Data
126   - public static class MidjourneyProperties {
  144 + public static class Midjourney {
127 145  
128 146 private String enable;
129 147 private String baseUrl;
... ... @@ -134,12 +152,21 @@ public class YudaoAiProperties {
134 152 }
135 153  
136 154 @Data
137   - public static class SunoProperties {
  155 + public static class Suno {
138 156  
139   - private boolean enable = false;
  157 + private boolean enable;
140 158  
141 159 private String baseUrl;
142 160  
143 161 }
144 162  
  163 + @Data
  164 + public static class WebSearch {
  165 +
  166 + private boolean enable;
  167 +
  168 + private String apiKey;
  169 +
  170 + }
  171 +
145 172 }
... ...
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/AiModelFactory.java renamed to yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/AiModelFactory.java
1   -package cn.iocoder.yudao.module.ai.framework.ai.core;
  1 +package cn.iocoder.yudao.module.ai.framework.ai.core.model;
2 2  
3 3 import cn.iocoder.yudao.module.ai.enums.model.AiPlatformEnum;
4 4 import cn.iocoder.yudao.module.ai.framework.ai.core.model.midjourney.api.MidjourneyApi;
... ...
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/AiModelFactoryImpl.java renamed to yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/AiModelFactoryImpl.java
1   -package cn.iocoder.yudao.module.ai.framework.ai.core;
  1 +package cn.iocoder.yudao.module.ai.framework.ai.core.model;
2 2  
3 3 import cn.hutool.core.io.FileUtil;
4 4 import cn.hutool.core.lang.Assert;
... ... @@ -14,6 +14,7 @@ import cn.iocoder.yudao.module.ai.framework.ai.config.AiAutoConfiguration;
14 14 import cn.iocoder.yudao.module.ai.framework.ai.config.YudaoAiProperties;
15 15 import cn.iocoder.yudao.module.ai.framework.ai.core.model.baichuan.BaiChuanChatModel;
16 16 import cn.iocoder.yudao.module.ai.framework.ai.core.model.doubao.DouBaoChatModel;
  17 +import cn.iocoder.yudao.module.ai.framework.ai.core.model.gemini.GeminiChatModel;
17 18 import cn.iocoder.yudao.module.ai.framework.ai.core.model.hunyuan.HunYuanChatModel;
18 19 import cn.iocoder.yudao.module.ai.framework.ai.core.model.midjourney.api.MidjourneyApi;
19 20 import cn.iocoder.yudao.module.ai.framework.ai.core.model.siliconflow.SiliconFlowApiConstants;
... ... @@ -67,6 +68,7 @@ import org.springframework.ai.minimax.MiniMaxChatOptions;
67 68 import org.springframework.ai.minimax.MiniMaxEmbeddingModel;
68 69 import org.springframework.ai.minimax.MiniMaxEmbeddingOptions;
69 70 import org.springframework.ai.minimax.api.MiniMaxApi;
  71 +import org.springframework.ai.model.anthropic.autoconfigure.AnthropicChatAutoConfiguration;
70 72 import org.springframework.ai.model.azure.openai.autoconfigure.AzureOpenAiChatAutoConfiguration;
71 73 import org.springframework.ai.model.azure.openai.autoconfigure.AzureOpenAiEmbeddingAutoConfiguration;
72 74 import org.springframework.ai.model.azure.openai.autoconfigure.AzureOpenAiEmbeddingProperties;
... ... @@ -93,6 +95,8 @@ import org.springframework.ai.openai.OpenAiImageModel;
93 95 import org.springframework.ai.openai.api.OpenAiApi;
94 96 import org.springframework.ai.openai.api.OpenAiImageApi;
95 97 import org.springframework.ai.openai.api.common.OpenAiApiConstants;
  98 +import org.springframework.ai.anthropic.AnthropicChatModel;
  99 +import org.springframework.ai.anthropic.api.AnthropicApi;
96 100 import org.springframework.ai.stabilityai.StabilityAiImageModel;
97 101 import org.springframework.ai.stabilityai.api.StabilityAiApi;
98 102 import org.springframework.ai.vectorstore.SimpleVectorStore;
... ... @@ -168,6 +172,10 @@ public class AiModelFactoryImpl implements AiModelFactory {
168 172 return buildOpenAiChatModel(apiKey, url);
169 173 case AZURE_OPENAI:
170 174 return buildAzureOpenAiChatModel(apiKey, url);
  175 + case ANTHROPIC:
  176 + return buildAnthropicChatModel(apiKey, url);
  177 + case GEMINI:
  178 + return buildGeminiChatModel(apiKey);
171 179 case OLLAMA:
172 180 return buildOllamaChatModel(url);
173 181 default:
... ... @@ -206,6 +214,10 @@ public class AiModelFactoryImpl implements AiModelFactory {
206 214 return SpringUtil.getBean(OpenAiChatModel.class);
207 215 case AZURE_OPENAI:
208 216 return SpringUtil.getBean(AzureOpenAiChatModel.class);
  217 + case ANTHROPIC:
  218 + return SpringUtil.getBean(AnthropicChatModel.class);
  219 + case GEMINI:
  220 + return SpringUtil.getBean(GeminiChatModel.class);
209 221 case OLLAMA:
210 222 return SpringUtil.getBean(OllamaChatModel.class);
211 223 default:
... ... @@ -260,7 +272,7 @@ public class AiModelFactoryImpl implements AiModelFactory {
260 272 String cacheKey = buildClientCacheKey(MidjourneyApi.class, AiPlatformEnum.MIDJOURNEY.getPlatform(), apiKey,
261 273 url);
262 274 return Singleton.get(cacheKey, (Func0<MidjourneyApi>) () -> {
263   - YudaoAiProperties.MidjourneyProperties properties = SpringUtil.getBean(YudaoAiProperties.class)
  275 + YudaoAiProperties.Midjourney properties = SpringUtil.getBean(YudaoAiProperties.class)
264 276 .getMidjourney();
265 277 return new MidjourneyApi(url, apiKey, properties.getNotifyUrl());
266 278 });
... ... @@ -347,7 +359,7 @@ public class AiModelFactoryImpl implements AiModelFactory {
347 359 * 可参考 {@link DashScopeImageAutoConfiguration} 的 dashScopeImageModel 方法
348 360 */
349 361 private static DashScopeImageModel buildTongYiImagesModel(String key) {
350   - DashScopeImageApi dashScopeImageApi = new DashScopeImageApi(key);
  362 + DashScopeImageApi dashScopeImageApi = DashScopeImageApi.builder().apiKey(key).build();
351 363 return DashScopeImageModel.builder()
352 364 .dashScopeApi(dashScopeImageApi)
353 365 .build();
... ... @@ -397,7 +409,7 @@ public class AiModelFactoryImpl implements AiModelFactory {
397 409 * 可参考 {@link AiAutoConfiguration#douBaoChatClient(YudaoAiProperties)}
398 410 */
399 411 private ChatModel buildDouBaoChatModel(String apiKey) {
400   - YudaoAiProperties.DouBaoProperties properties = new YudaoAiProperties.DouBaoProperties()
  412 + YudaoAiProperties.DouBao properties = new YudaoAiProperties.DouBao()
401 413 .setApiKey(apiKey);
402 414 return new AiAutoConfiguration().buildDouBaoChatClient(properties);
403 415 }
... ... @@ -406,7 +418,7 @@ public class AiModelFactoryImpl implements AiModelFactory {
406 418 * 可参考 {@link AiAutoConfiguration#hunYuanChatClient(YudaoAiProperties)}
407 419 */
408 420 private ChatModel buildHunYuanChatModel(String apiKey, String url) {
409   - YudaoAiProperties.HunYuanProperties properties = new YudaoAiProperties.HunYuanProperties()
  421 + YudaoAiProperties.HunYuan properties = new YudaoAiProperties.HunYuan()
410 422 .setBaseUrl(url).setApiKey(apiKey);
411 423 return new AiAutoConfiguration().buildHunYuanChatClient(properties);
412 424 }
... ... @@ -415,7 +427,7 @@ public class AiModelFactoryImpl implements AiModelFactory {
415 427 * 可参考 {@link AiAutoConfiguration#siliconFlowChatClient(YudaoAiProperties)}
416 428 */
417 429 private ChatModel buildSiliconFlowChatModel(String apiKey) {
418   - YudaoAiProperties.SiliconFlowProperties properties = new YudaoAiProperties.SiliconFlowProperties()
  430 + YudaoAiProperties.SiliconFlow properties = new YudaoAiProperties.SiliconFlow()
419 431 .setApiKey(apiKey);
420 432 return new AiAutoConfiguration().buildSiliconFlowChatClient(properties);
421 433 }
... ... @@ -473,7 +485,7 @@ public class AiModelFactoryImpl implements AiModelFactory {
473 485 private static XingHuoChatModel buildXingHuoChatModel(String key) {
474 486 List<String> keys = StrUtil.split(key, '|');
475 487 Assert.equals(keys.size(), 2, "XingHuoChatClient 的密钥需要 (appKey|secretKey) 格式");
476   - YudaoAiProperties.XingHuoProperties properties = new YudaoAiProperties.XingHuoProperties()
  488 + YudaoAiProperties.XingHuo properties = new YudaoAiProperties.XingHuo()
477 489 .setAppKey(keys.get(0)).setSecretKey(keys.get(1));
478 490 return new AiAutoConfiguration().buildXingHuoChatClient(properties);
479 491 }
... ... @@ -482,7 +494,7 @@ public class AiModelFactoryImpl implements AiModelFactory {
482 494 * 可参考 {@link AiAutoConfiguration#baiChuanChatClient(YudaoAiProperties)}
483 495 */
484 496 private BaiChuanChatModel buildBaiChuanChatModel(String apiKey) {
485   - YudaoAiProperties.BaiChuanProperties properties = new YudaoAiProperties.BaiChuanProperties()
  497 + YudaoAiProperties.BaiChuan properties = new YudaoAiProperties.BaiChuan()
486 498 .setApiKey(apiKey);
487 499 return new AiAutoConfiguration().buildBaiChuanChatClient(properties);
488 500 }
... ... @@ -513,6 +525,30 @@ public class AiModelFactoryImpl implements AiModelFactory {
513 525 }
514 526  
515 527 /**
  528 + * 可参考 {@link AnthropicChatAutoConfiguration} 的 anthropicApi 方法
  529 + */
  530 + private static AnthropicChatModel buildAnthropicChatModel(String apiKey, String url) {
  531 + AnthropicApi.Builder builder = AnthropicApi.builder().apiKey(apiKey);
  532 + if (StrUtil.isNotEmpty(url)) {
  533 + builder.baseUrl(url);
  534 + }
  535 + AnthropicApi anthropicApi = builder.build();
  536 + return AnthropicChatModel.builder()
  537 + .anthropicApi(anthropicApi)
  538 + .toolCallingManager(getToolCallingManager())
  539 + .build();
  540 + }
  541 +
  542 + /**
  543 + * 可参考 {@link AiAutoConfiguration#buildGeminiChatClient(YudaoAiProperties.Gemini)}
  544 + */
  545 + private static GeminiChatModel buildGeminiChatModel(String apiKey) {
  546 + YudaoAiProperties.Gemini properties = SpringUtil.getBean(YudaoAiProperties.class)
  547 + .getGemini().setApiKey(apiKey);
  548 + return new AiAutoConfiguration().buildGeminiChatClient(properties);
  549 + }
  550 +
  551 + /**
516 552 * 可参考 {@link OpenAiImageAutoConfiguration} 的 openAiImageModel 方法
517 553 */
518 554 private OpenAiImageModel buildOpenAiImageModel(String openAiToken, String url) {
... ...
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/doubao/DouBaoChatModel.java
... ... @@ -6,7 +6,6 @@ import org.springframework.ai.chat.model.ChatModel;
6 6 import org.springframework.ai.chat.model.ChatResponse;
7 7 import org.springframework.ai.chat.prompt.ChatOptions;
8 8 import org.springframework.ai.chat.prompt.Prompt;
9   -import org.springframework.ai.openai.OpenAiChatModel;
10 9 import reactor.core.publisher.Flux;
11 10  
12 11 /**
... ... @@ -19,13 +18,14 @@ import reactor.core.publisher.Flux;
19 18 public class DouBaoChatModel implements ChatModel {
20 19  
21 20 public static final String BASE_URL = "https://ark.cn-beijing.volces.com/api";
  21 + public static final String COMPLETE_PATH = "/v3/chat/completions";
22 22  
23 23 public static final String MODEL_DEFAULT = "doubao-1-5-lite-32k-250115";
24 24  
25 25 /**
26 26 * 兼容 OpenAI 接口,进行复用
27 27 */
28   - private final OpenAiChatModel openAiChatModel;
  28 + private final ChatModel openAiChatModel;
29 29  
30 30 @Override
31 31 public ChatResponse call(Prompt prompt) {
... ...
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/gemini/GeminiChatModel.java 0 → 100644
  1 +package cn.iocoder.yudao.module.ai.framework.ai.core.model.gemini;
  2 +
  3 +import lombok.RequiredArgsConstructor;
  4 +import lombok.extern.slf4j.Slf4j;
  5 +import org.springframework.ai.chat.model.ChatModel;
  6 +import org.springframework.ai.chat.model.ChatResponse;
  7 +import org.springframework.ai.chat.prompt.ChatOptions;
  8 +import org.springframework.ai.chat.prompt.Prompt;
  9 +import org.springframework.ai.openai.OpenAiChatModel;
  10 +import reactor.core.publisher.Flux;
  11 +
  12 +/**
  13 + * 谷歌 Gemini {@link ChatModel} 实现类,基于 Google AI Studio 提供的 <a href="https://ai.google.dev/gemini-api/docs/openai">OpenAI 兼容方案</a>
  14 + *
  15 + * @author 芋道源码
  16 + */
  17 +@Slf4j
  18 +@RequiredArgsConstructor
  19 +public class GeminiChatModel implements ChatModel {
  20 +
  21 + public static final String BASE_URL = "https://generativelanguage.googleapis.com/v1beta/openai/";
  22 + public static final String COMPLETE_PATH = "/chat/completions";
  23 +
  24 + public static final String MODEL_DEFAULT = "gemini-2.5-flash";
  25 +
  26 + /**
  27 + * 兼容 OpenAI 接口,进行复用
  28 + */
  29 + private final OpenAiChatModel openAiChatModel;
  30 +
  31 + @Override
  32 + public ChatResponse call(Prompt prompt) {
  33 + return openAiChatModel.call(prompt);
  34 + }
  35 +
  36 + @Override
  37 + public Flux<ChatResponse> stream(Prompt prompt) {
  38 + return openAiChatModel.stream(prompt);
  39 + }
  40 +
  41 + @Override
  42 + public ChatOptions getDefaultOptions() {
  43 + return openAiChatModel.getDefaultOptions();
  44 + }
  45 +
  46 +}
... ...
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/hunyuan/HunYuanChatModel.java
... ... @@ -6,7 +6,6 @@ import org.springframework.ai.chat.model.ChatModel;
6 6 import org.springframework.ai.chat.model.ChatResponse;
7 7 import org.springframework.ai.chat.prompt.ChatOptions;
8 8 import org.springframework.ai.chat.prompt.Prompt;
9   -import org.springframework.ai.openai.OpenAiChatModel;
10 9 import reactor.core.publisher.Flux;
11 10  
12 11 /**
... ... @@ -22,6 +21,7 @@ import reactor.core.publisher.Flux;
22 21 public class HunYuanChatModel implements ChatModel {
23 22  
24 23 public static final String BASE_URL = "https://api.hunyuan.cloud.tencent.com";
  24 + public static final String COMPLETE_PATH = "/v1/chat/completions";
25 25  
26 26 public static final String MODEL_DEFAULT = "hunyuan-turbo";
27 27  
... ... @@ -32,7 +32,7 @@ public class HunYuanChatModel implements ChatModel {
32 32 /**
33 33 * 兼容 OpenAI 接口,进行复用
34 34 */
35   - private final OpenAiChatModel openAiChatModel;
  35 + private final ChatModel openAiChatModel;
36 36  
37 37 @Override
38 38 public ChatResponse call(Prompt prompt) {
... ...
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/siliconflow/SiliconFlowChatModel.java
... ... @@ -23,7 +23,7 @@ public class SiliconFlowChatModel implements ChatModel {
23 23 /**
24 24 * 兼容 OpenAI 接口,进行复用
25 25 */
26   - private final OpenAiChatModel openAiChatModel;
  26 + private final ChatModel openAiChatModel;
27 27  
28 28 @Override
29 29 public ChatResponse call(Prompt prompt) {
... ...
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/xinghuo/XingHuoChatModel.java
... ... @@ -6,7 +6,6 @@ import org.springframework.ai.chat.model.ChatModel;
6 6 import org.springframework.ai.chat.model.ChatResponse;
7 7 import org.springframework.ai.chat.prompt.ChatOptions;
8 8 import org.springframework.ai.chat.prompt.Prompt;
9   -import org.springframework.ai.openai.OpenAiChatModel;
10 9 import reactor.core.publisher.Flux;
11 10  
12 11 /**
... ... @@ -18,28 +17,34 @@ import reactor.core.publisher.Flux;
18 17 @RequiredArgsConstructor
19 18 public class XingHuoChatModel implements ChatModel {
20 19  
21   - public static final String BASE_URL = "https://spark-api-open.xf-yun.com";
  20 + public static final String BASE_URL_V1 = "https://spark-api-open.xf-yun.com";
22 21  
23   - public static final String MODEL_DEFAULT = "generalv3.5";
  22 + public static final String BASE_URL_V2 = "https://spark-api-open.xf-yun.com";
  23 + public static final String BASE_COMPLETIONS_PATH_V2 = "/v2/chat/completions";
24 24  
25 25 /**
26   - * 兼容 OpenAI 接口,进行复用
  26 + * 已知模型名列表:x1、4.0Ultra、generalv3.5、max-32k、generalv3、pro-128k、lite
27 27 */
28   - private final OpenAiChatModel openAiChatModel;
  28 + public static final String MODEL_DEFAULT = "4.0Ultra";
  29 +
  30 + /**
  31 + * v1 兼容 OpenAI 接口,进行复用
  32 + */
  33 + private final ChatModel openAiChatModelV1;
29 34  
30 35 @Override
31 36 public ChatResponse call(Prompt prompt) {
32   - return openAiChatModel.call(prompt);
  37 + return openAiChatModelV1.call(prompt);
33 38 }
34 39  
35 40 @Override
36 41 public Flux<ChatResponse> stream(Prompt prompt) {
37   - return openAiChatModel.stream(prompt);
  42 + return openAiChatModelV1.stream(prompt);
38 43 }
39 44  
40 45 @Override
41 46 public ChatOptions getDefaultOptions() {
42   - return openAiChatModel.getDefaultOptions();
  47 + return openAiChatModelV1.getDefaultOptions();
43 48 }
44 49  
45 50 }
... ...
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/webserch/AiWebSearchClient.java 0 → 100644
  1 +package cn.iocoder.yudao.module.ai.framework.ai.core.webserch;
  2 +
  3 +/**
  4 + * 网络搜索客户端接口
  5 + *
  6 + * @author 芋道源码
  7 + */
  8 +public interface AiWebSearchClient {
  9 +
  10 + /**
  11 + * 网页搜索
  12 + *
  13 + * @param request 搜索请求
  14 + * @return 搜索结果
  15 + */
  16 + AiWebSearchResponse search(AiWebSearchRequest request);
  17 +
  18 +}
... ...
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/webserch/AiWebSearchRequest.java 0 → 100644
  1 +package cn.iocoder.yudao.module.ai.framework.ai.core.webserch;
  2 +
  3 +import jakarta.validation.constraints.Max;
  4 +import jakarta.validation.constraints.Min;
  5 +import jakarta.validation.constraints.NotEmpty;
  6 +import jakarta.validation.constraints.NotNull;
  7 +import lombok.Data;
  8 +
  9 +@Data
  10 +public class AiWebSearchRequest {
  11 +
  12 + /**
  13 + * 用户的搜索词
  14 + */
  15 + @NotEmpty(message = "搜索词不能为空")
  16 + private String query;
  17 +
  18 + /**
  19 + * 是否显示文本摘要
  20 + *
  21 + * true - 显示
  22 + * false - 不显示(默认)
  23 + */
  24 + private Boolean summary;
  25 +
  26 + /**
  27 + * 返回结果的条数
  28 + */
  29 + @NotNull(message = "返回结果条数不能为空")
  30 + @Min(message = "返回结果条数最小为 1", value = 1)
  31 + @Max(message = "返回结果条数最大为 50", value = 50)
  32 + private Integer count;
  33 +
  34 +}
... ...
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/webserch/AiWebSearchResponse.java 0 → 100644
  1 +package cn.iocoder.yudao.module.ai.framework.ai.core.webserch;
  2 +
  3 +import lombok.Data;
  4 +
  5 +import java.util.List;
  6 +
  7 +@Data
  8 +public class AiWebSearchResponse {
  9 +
  10 + /**
  11 + * 总数(总共匹配的网页数)
  12 + */
  13 + private Long total;
  14 +
  15 + /**
  16 + * 数据列表
  17 + */
  18 + private List<WebPage> lists;
  19 +
  20 + /**
  21 + * 网页对象
  22 + */
  23 + @Data
  24 + public static class WebPage {
  25 +
  26 + /**
  27 + * 名称
  28 + *
  29 + * 例如说:搜狐网
  30 + */
  31 + private String name;
  32 + /**
  33 + * 图标
  34 + */
  35 + private String icon;
  36 +
  37 + /**
  38 + * 标题
  39 + *
  40 + * 例如说:186页|阿里巴巴:2024年环境、社会和治理(ESG)报告
  41 + */
  42 + private String title;
  43 + /**
  44 + * URL
  45 + *
  46 + * 例如说:https://m.sohu.com/a/815036254_121819701/?pvid=000115_3w_a
  47 + */
  48 + @SuppressWarnings("JavadocLinkAsPlainText")
  49 + private String url;
  50 +
  51 + /**
  52 + * 内容的简短描述
  53 + */
  54 + private String snippet;
  55 + /**
  56 + * 内容的文本摘要
  57 + */
  58 + private String summary;
  59 +
  60 + }
  61 +
  62 +}
0 63 \ No newline at end of file
... ...
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/webserch/bocha/AiBoChaWebSearchClient.java 0 → 100644
  1 +package cn.iocoder.yudao.module.ai.framework.ai.core.webserch.bocha;
  2 +
  3 +import cn.hutool.core.collection.CollUtil;
  4 +import cn.hutool.core.lang.Assert;
  5 +import cn.iocoder.yudao.framework.common.pojo.CommonResult;
  6 +import cn.iocoder.yudao.module.ai.framework.ai.core.webserch.AiWebSearchClient;
  7 +import cn.iocoder.yudao.module.ai.framework.ai.core.webserch.AiWebSearchRequest;
  8 +import cn.iocoder.yudao.module.ai.framework.ai.core.webserch.AiWebSearchResponse;
  9 +import com.fasterxml.jackson.annotation.JsonInclude;
  10 +import lombok.extern.slf4j.Slf4j;
  11 +import org.springframework.core.ParameterizedTypeReference;
  12 +import org.springframework.http.HttpStatusCode;
  13 +import org.springframework.http.MediaType;
  14 +import org.springframework.web.reactive.function.client.ClientResponse;
  15 +import org.springframework.web.reactive.function.client.WebClient;
  16 +import reactor.core.publisher.Mono;
  17 +
  18 +import java.util.List;
  19 +import java.util.function.Function;
  20 +import java.util.function.Predicate;
  21 +
  22 +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
  23 +
  24 +/**
  25 + * 博查 {@link AiWebSearchClient} 实现类
  26 + *
  27 + * @see <a href="https://open.bochaai.com/overview">博查 AI 开放平台</a>
  28 + *
  29 + * @author 芋道源码
  30 + */
  31 +@Slf4j
  32 +public class AiBoChaWebSearchClient implements AiWebSearchClient {
  33 +
  34 + public static final String BASE_URL = "https://api.bochaai.com";
  35 + private static final String AUTHORIZATION_HEADER = "Authorization";
  36 + private static final String BEARER_PREFIX = "Bearer ";
  37 +
  38 + private final WebClient webClient;
  39 +
  40 + private final Predicate<HttpStatusCode> STATUS_PREDICATE = status -> !status.is2xxSuccessful();
  41 +
  42 + private final Function<Object, Function<ClientResponse, Mono<? extends Throwable>>> EXCEPTION_FUNCTION =
  43 + reqParam -> response -> response.bodyToMono(String.class).handle((responseBody, sink) -> {
  44 + log.error("[AiBoChaWebSearchClient] 调用失败!请求参数:[{}],响应数据: [{}]", reqParam, responseBody);
  45 + sink.error(new IllegalStateException("[AiBoChaWebSearchClient] 调用失败!"));
  46 + });
  47 +
  48 + public AiBoChaWebSearchClient(String apiKey) {
  49 + this.webClient = WebClient.builder()
  50 + .baseUrl(BASE_URL)
  51 + .defaultHeaders((headers) -> {
  52 + headers.setContentType(MediaType.APPLICATION_JSON);
  53 + headers.add(AUTHORIZATION_HEADER, BEARER_PREFIX + apiKey);
  54 + })
  55 + .build();
  56 + }
  57 +
  58 + @Override
  59 + public AiWebSearchResponse search(AiWebSearchRequest request) {
  60 + // 转换请求参数
  61 + WebSearchRequest webSearchRequest = new WebSearchRequest(
  62 + request.getQuery(),
  63 + request.getSummary(),
  64 + request.getCount()
  65 + );
  66 + // 调用博查 API
  67 + CommonResult<WebSearchResponse> response = this.webClient.post()
  68 + .uri("/v1/web-search")
  69 + .bodyValue(webSearchRequest)
  70 + .retrieve()
  71 + .onStatus(STATUS_PREDICATE, EXCEPTION_FUNCTION.apply(webSearchRequest))
  72 + .bodyToMono(new ParameterizedTypeReference<CommonResult<WebSearchResponse>>() {})
  73 + .block();
  74 + if (response == null) {
  75 + throw new IllegalStateException("[search][搜索结果为空]");
  76 + }
  77 + if (response.getData() == null) {
  78 + throw new IllegalStateException(String.format("[search][搜索失败,code = %s, msg = %s]",
  79 + response.getCode(), response.getMsg()));
  80 + }
  81 + WebSearchResponse data = response.getData();
  82 +
  83 + // 转换结果
  84 + AiWebSearchResponse result = new AiWebSearchResponse();
  85 + if (data.webPages() == null || CollUtil.isEmpty(data.webPages().value())) {
  86 + return result.setTotal(0L).setLists(List.of());
  87 + }
  88 + return result.setTotal(data.webPages().totalEstimatedMatches())
  89 + .setLists(convertList(data.webPages().value(), page -> new AiWebSearchResponse.WebPage()
  90 + .setName(page.siteName()).setIcon(page.siteIcon())
  91 + .setTitle(page.name()).setUrl(page.url())
  92 + .setSnippet(page.snippet()).setSummary(page.summary())));
  93 + }
  94 +
  95 + /**
  96 + * 网页搜索请求参数
  97 + */
  98 + @JsonInclude(value = JsonInclude.Include.NON_NULL)
  99 + public record WebSearchRequest(
  100 + String query,
  101 + Boolean summary,
  102 + Integer count
  103 + ) {
  104 + public WebSearchRequest {
  105 + Assert.notBlank(query, "query 不能为空");
  106 + }
  107 + }
  108 +
  109 + /**
  110 + * 网页搜索响应
  111 + */
  112 + @JsonInclude(value = JsonInclude.Include.NON_NULL)
  113 + public record WebSearchResponse(
  114 + WebSearchWebPages webPages
  115 + ) {
  116 + }
  117 +
  118 + /**
  119 + * 网页搜索结果
  120 + */
  121 + @JsonInclude(value = JsonInclude.Include.NON_NULL)
  122 + public record WebSearchWebPages(
  123 + String webSearchUrl,
  124 + Long totalEstimatedMatches,
  125 + List<WebPageValue> value,
  126 + Boolean someResultsRemoved
  127 + ) {
  128 +
  129 + /**
  130 + * 网页结果值
  131 + */
  132 + @JsonInclude(value = JsonInclude.Include.NON_NULL)
  133 + public record WebPageValue(
  134 + String id,
  135 + String name,
  136 + String url,
  137 + String displayUrl,
  138 + String snippet,
  139 + String summary,
  140 + String siteName,
  141 + String siteIcon,
  142 + String datePublished,
  143 + String dateLastCrawled,
  144 + String cachedPageUrl,
  145 + String language,
  146 + Boolean isFamilyFriendly,
  147 + Boolean isNavigational
  148 + ) {
  149 + }
  150 +
  151 + }
  152 +
  153 +}
... ...
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/security/config/SecurityConfiguration.java 0 → 100644
  1 +package cn.iocoder.yudao.module.ai.framework.security.config;
  2 +
  3 +import cn.iocoder.yudao.framework.security.config.AuthorizeRequestsCustomizer;
  4 +import jakarta.annotation.Resource;
  5 +import org.springframework.ai.mcp.server.autoconfigure.McpServerProperties;
  6 +import org.springframework.context.annotation.Bean;
  7 +import org.springframework.context.annotation.Configuration;
  8 +import org.springframework.security.config.annotation.web.builders.HttpSecurity;
  9 +import org.springframework.security.config.annotation.web.configurers.AuthorizeHttpRequestsConfigurer;
  10 +
  11 +/**
  12 + * AI 模块的 Security 配置
  13 + */
  14 +@Configuration(proxyBeanMethods = false, value = "aiSecurityConfiguration")
  15 +public class SecurityConfiguration {
  16 +
  17 + @Resource
  18 + private McpServerProperties serverProperties;
  19 +
  20 + @Bean("aiAuthorizeRequestsCustomizer")
  21 + public AuthorizeRequestsCustomizer authorizeRequestsCustomizer() {
  22 + return new AuthorizeRequestsCustomizer() {
  23 +
  24 + @Override
  25 + public void customize(AuthorizeHttpRequestsConfigurer<HttpSecurity>.AuthorizationManagerRequestMatcherRegistry registry) {
  26 + // MCP Server
  27 + registry.requestMatchers(serverProperties.getSseEndpoint()).permitAll();
  28 + registry.requestMatchers(serverProperties.getSseMessageEndpoint()).permitAll();
  29 + }
  30 +
  31 + };
  32 + }
  33 +
  34 +}
... ...
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/security/core/package-info.java 0 → 100644
  1 +/**
  2 + * 占位
  3 + */
  4 +package cn.iocoder.yudao.module.ai.framework.security.core;
... ...
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/service/chat/AiChatMessageServiceImpl.java
1 1 package cn.iocoder.yudao.module.ai.service.chat;
2 2  
  3 +import cn.hutool.core.codec.Base64;
3 4 import cn.hutool.core.collection.CollUtil;
  5 +import cn.hutool.core.io.file.FileNameUtil;
4 6 import cn.hutool.core.util.ObjUtil;
5 7 import cn.hutool.core.util.StrUtil;
6   -import cn.iocoder.yudao.module.ai.enums.model.AiPlatformEnum;
7   -import cn.iocoder.yudao.module.ai.util.AiUtils;
  8 +import cn.hutool.http.HttpUtil;
8 9 import cn.iocoder.yudao.framework.common.pojo.CommonResult;
9 10 import cn.iocoder.yudao.framework.common.pojo.PageResult;
10 11 import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
... ... @@ -21,6 +22,10 @@ import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiModelDO;
21 22 import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiToolDO;
22 23 import cn.iocoder.yudao.module.ai.dal.mysql.chat.AiChatMessageMapper;
23 24 import cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants;
  25 +import cn.iocoder.yudao.module.ai.enums.model.AiPlatformEnum;
  26 +import cn.iocoder.yudao.module.ai.framework.ai.core.webserch.AiWebSearchClient;
  27 +import cn.iocoder.yudao.module.ai.framework.ai.core.webserch.AiWebSearchRequest;
  28 +import cn.iocoder.yudao.module.ai.framework.ai.core.webserch.AiWebSearchResponse;
24 29 import cn.iocoder.yudao.module.ai.service.knowledge.AiKnowledgeDocumentService;
25 30 import cn.iocoder.yudao.module.ai.service.knowledge.AiKnowledgeSegmentService;
26 31 import cn.iocoder.yudao.module.ai.service.knowledge.bo.AiKnowledgeSegmentSearchReqBO;
... ... @@ -28,6 +33,10 @@ import cn.iocoder.yudao.module.ai.service.knowledge.bo.AiKnowledgeSegmentSearchR
28 33 import cn.iocoder.yudao.module.ai.service.model.AiChatRoleService;
29 34 import cn.iocoder.yudao.module.ai.service.model.AiModelService;
30 35 import cn.iocoder.yudao.module.ai.service.model.AiToolService;
  36 +import cn.iocoder.yudao.module.ai.util.AiUtils;
  37 +import cn.iocoder.yudao.module.infra.framework.file.core.utils.FileTypeUtils;
  38 +import com.google.common.collect.Maps;
  39 +import io.modelcontextprotocol.client.McpSyncClient;
31 40 import jakarta.annotation.Resource;
32 41 import lombok.extern.slf4j.Slf4j;
33 42 import org.springframework.ai.chat.messages.Message;
... ... @@ -39,6 +48,11 @@ import org.springframework.ai.chat.model.ChatResponse;
39 48 import org.springframework.ai.chat.model.StreamingChatModel;
40 49 import org.springframework.ai.chat.prompt.ChatOptions;
41 50 import org.springframework.ai.chat.prompt.Prompt;
  51 +import org.springframework.ai.mcp.SyncMcpToolCallbackProvider;
  52 +import org.springframework.ai.mcp.client.autoconfigure.properties.McpClientCommonProperties;
  53 +import org.springframework.ai.tool.ToolCallback;
  54 +import org.springframework.ai.tool.resolution.ToolCallbackResolver;
  55 +import org.springframework.beans.factory.annotation.Autowired;
42 56 import org.springframework.stereotype.Service;
43 57 import org.springframework.transaction.annotation.Transactional;
44 58 import reactor.core.publisher.Flux;
... ... @@ -65,12 +79,31 @@ import static cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants.CHAT_MESSAGE_N
65 79 public class AiChatMessageServiceImpl implements AiChatMessageService {
66 80  
67 81 /**
  82 + * 联网搜索的结束数
  83 + */
  84 + private static final Integer WEB_SEARCH_COUNT = 10;
  85 +
  86 + // TODO @芋艿:后续优化下对话的 Prompt 整体结构
  87 +
  88 + /**
68 89 * 知识库转 {@link UserMessage} 的内容模版
69 90 */
70 91 private static final String KNOWLEDGE_USER_MESSAGE_TEMPLATE = "使用 <Reference></Reference> 标记中的内容作为本次对话的参考:\n\n" +
71 92 "%s\n\n" + // 多个 <Reference></Reference> 的拼接
72 93 "回答要求:\n- 避免提及你是从 <Reference></Reference> 获取的知识。";
73 94  
  95 + private static final String WEB_SEARCH_USER_MESSAGE_TEMPLATE = "使用 <WebSearch></WebSearch> 标记中的内容作为本次对话的参考:\n\n" +
  96 + "%s\n\n" + // 多个 <WebSearch></WebSearch> 的拼接
  97 + "回答要求:\n- 避免提及你是从 <WebSearch></WebSearch> 获取的知识。";
  98 +
  99 + /**
  100 + * 附件转 ${@link UserMessage} 的内容模版
  101 + */
  102 + @SuppressWarnings("TextBlockMigration")
  103 + private static final String Attachment_USER_MESSAGE_TEMPLATE = "使用 <Attachment></Attachment> 标记用户对话上传的附件内容:\n\n" +
  104 + "%s\n\n" + // 多个 <Attachment></Attachment> 的拼接
  105 + "回答要求:\n- 避免提及 <Attachment></Attachment> 附件的编码格式。";
  106 +
74 107 @Resource
75 108 private AiChatMessageMapper chatMessageMapper;
76 109  
... ... @@ -87,6 +120,21 @@ public class AiChatMessageServiceImpl implements AiChatMessageService {
87 120 @Resource
88 121 private AiToolService toolService;
89 122  
  123 + @SuppressWarnings("SpringJavaAutowiredFieldsWarningInspection")
  124 + @Autowired(required = false) // 由于 yudao.ai.web-search.enable 配置项,可以关闭 AiWebSearchClient 的功能,所以这里只能不强制注入
  125 + private AiWebSearchClient webSearchClient;
  126 +
  127 + @SuppressWarnings("SpringJavaAutowiredFieldsWarningInspection")
  128 + @Autowired(required = false) // 由于 yudao.ai.mcp.client.enable 配置项,可以关闭 McpSyncClient 的功能,所以这里只能不强制注入
  129 + private List<McpSyncClient> mcpClients;
  130 +
  131 + @SuppressWarnings("SpringJavaAutowiredFieldsWarningInspection")
  132 + @Autowired(required = false) // 由于 yudao.ai.mcp.client.enable 配置项,可以关闭 McpSyncClient 的功能,所以这里只能不强制注入
  133 + private McpClientCommonProperties mcpClientCommonProperties;
  134 +
  135 + @Resource
  136 + private ToolCallbackResolver toolCallbackResolver;
  137 +
90 138 @Transactional(rollbackFor = Exception.class)
91 139 public AiChatMessageSendRespVO sendMessage(AiChatMessageSendReqVO sendReqVO, Long userId) {
92 140 // 1.1 校验对话存在
... ... @@ -100,27 +148,35 @@ public class AiChatMessageServiceImpl implements AiChatMessageService {
100 148 AiModelDO model = modalService.validateModel(conversation.getModelId());
101 149 ChatModel chatModel = modalService.getChatModel(model.getId());
102 150  
103   - // 2. 知识库找回
104   - List<AiKnowledgeSegmentSearchRespBO> knowledgeSegments = recallKnowledgeSegment(sendReqVO.getContent(), conversation);
  151 + // 2.1 知识库召回
  152 + List<AiKnowledgeSegmentSearchRespBO> knowledgeSegments = recallKnowledgeSegment(
  153 + sendReqVO.getContent(), conversation);
  154 +
  155 + // 2.2 联网搜索
  156 + AiWebSearchResponse webSearchResponse = Boolean.TRUE.equals(sendReqVO.getUseSearch()) && webSearchClient != null ?
  157 + webSearchClient.search(new AiWebSearchRequest().setQuery(sendReqVO.getContent())
  158 + .setSummary(true).setCount(WEB_SEARCH_COUNT)) : null;
105 159  
106 160 // 3. 插入 user 发送消息
107 161 AiChatMessageDO userMessage = createChatMessage(conversation.getId(), null, model,
108 162 userId, conversation.getRoleId(), MessageType.USER, sendReqVO.getContent(), sendReqVO.getUseContext(),
109   - null);
  163 + null, sendReqVO.getAttachmentUrls(), null);
110 164  
111   - // 3.1 插入 assistant 接收消息
  165 + // 4.1 插入 assistant 接收消息
112 166 AiChatMessageDO assistantMessage = createChatMessage(conversation.getId(), userMessage.getId(), model,
113 167 userId, conversation.getRoleId(), MessageType.ASSISTANT, "", sendReqVO.getUseContext(),
114   - knowledgeSegments);
  168 + knowledgeSegments, null, webSearchResponse);
115 169  
116   - // 3.2 创建 chat 需要的 Prompt
117   - Prompt prompt = buildPrompt(conversation, historyMessages, knowledgeSegments, model, sendReqVO);
  170 + // 4.2 创建 chat 需要的 Prompt
  171 + Prompt prompt = buildPrompt(conversation, historyMessages, knowledgeSegments, webSearchResponse, model, sendReqVO);
118 172 ChatResponse chatResponse = chatModel.call(prompt);
119 173  
120   - // 3.3 更新响应内容
121   - String newContent = chatResponse.getResult().getOutput().getText();
122   - chatMessageMapper.updateById(new AiChatMessageDO().setId(assistantMessage.getId()).setContent(newContent));
123   - // 3.4 响应结果
  174 + // 4.3 更新响应内容
  175 + String newContent = AiUtils.getChatResponseContent(chatResponse);
  176 + String newReasoningContent = AiUtils.getChatResponseReasoningContent(chatResponse);
  177 + chatMessageMapper.updateById(new AiChatMessageDO().setId(assistantMessage.getId())
  178 + .setContent(newContent).setReasoningContent(newReasoningContent));
  179 + // 4.4 响应结果
124 180 Map<Long, AiKnowledgeDocumentDO> documentMap = knowledgeDocumentService.getKnowledgeDocumentMap(
125 181 convertSet(knowledgeSegments, AiKnowledgeSegmentSearchRespBO::getDocumentId));
126 182 List<AiChatMessageRespVO.KnowledgeSegment> segments = BeanUtils.toBean(knowledgeSegments,
... ... @@ -131,7 +187,8 @@ public class AiChatMessageServiceImpl implements AiChatMessageService {
131 187 return new AiChatMessageSendRespVO()
132 188 .setSend(BeanUtils.toBean(userMessage, AiChatMessageSendRespVO.Message.class))
133 189 .setReceive(BeanUtils.toBean(assistantMessage, AiChatMessageSendRespVO.Message.class)
134   - .setContent(newContent).setSegments(segments));
  190 + .setContent(newContent).setSegments(segments)
  191 + .setWebSearchPages(webSearchResponse != null ? webSearchResponse.getLists() : null));
135 192 }
136 193  
137 194 @Override
... ... @@ -148,29 +205,36 @@ public class AiChatMessageServiceImpl implements AiChatMessageService {
148 205 AiModelDO model = modalService.validateModel(conversation.getModelId());
149 206 StreamingChatModel chatModel = modalService.getChatModel(model.getId());
150 207  
151   - // 2. 知识库找回
152   - List<AiKnowledgeSegmentSearchRespBO> knowledgeSegments = recallKnowledgeSegment(sendReqVO.getContent(),
153   - conversation);
  208 + // 2.1 知识库找回
  209 + List<AiKnowledgeSegmentSearchRespBO> knowledgeSegments = recallKnowledgeSegment(
  210 + sendReqVO.getContent(), conversation);
  211 +
  212 + // 2.2 联网搜索
  213 + AiWebSearchResponse webSearchResponse = Boolean.TRUE.equals(sendReqVO.getUseSearch()) && webSearchClient != null ?
  214 + webSearchClient.search(new AiWebSearchRequest().setQuery(sendReqVO.getContent())
  215 + .setSummary(true).setCount(WEB_SEARCH_COUNT)) : null;
154 216  
155 217 // 3. 插入 user 发送消息
156 218 AiChatMessageDO userMessage = createChatMessage(conversation.getId(), null, model,
157 219 userId, conversation.getRoleId(), MessageType.USER, sendReqVO.getContent(), sendReqVO.getUseContext(),
158   - null);
  220 + null, sendReqVO.getAttachmentUrls(), null);
159 221  
160 222 // 4.1 插入 assistant 接收消息
161 223 AiChatMessageDO assistantMessage = createChatMessage(conversation.getId(), userMessage.getId(), model,
162 224 userId, conversation.getRoleId(), MessageType.ASSISTANT, "", sendReqVO.getUseContext(),
163   - knowledgeSegments);
  225 + knowledgeSegments, null, webSearchResponse);
164 226  
165 227 // 4.2 构建 Prompt,并进行调用
166   - Prompt prompt = buildPrompt(conversation, historyMessages, knowledgeSegments, model, sendReqVO);
  228 + Prompt prompt = buildPrompt(conversation, historyMessages, knowledgeSegments, webSearchResponse, model, sendReqVO);
167 229 Flux<ChatResponse> streamResponse = chatModel.stream(prompt);
168 230  
169 231 // 4.3 流式返回
170 232 StringBuffer contentBuffer = new StringBuffer();
  233 + StringBuffer reasoningContentBuffer = new StringBuffer();
171 234 return streamResponse.map(chunk -> {
172   - // 处理知识库的返回,只有首次才有
  235 + // 仅首次:返回知识库、联网搜索
173 236 List<AiChatMessageRespVO.KnowledgeSegment> segments = null;
  237 + List<AiWebSearchResponse.WebPage> webSearchPages = null;
174 238 if (StrUtil.isEmpty(contentBuffer)) {
175 239 Map<Long, AiKnowledgeDocumentDO> documentMap = TenantUtils.executeIgnore(() ->
176 240 knowledgeDocumentService.getKnowledgeDocumentMap(
... ... @@ -179,24 +243,56 @@ public class AiChatMessageServiceImpl implements AiChatMessageService {
179 243 AiKnowledgeDocumentDO document = documentMap.get(segment.getDocumentId());
180 244 segment.setDocumentName(document != null ? document.getName() : null);
181 245 });
  246 + if (webSearchResponse != null) {
  247 + webSearchPages = webSearchResponse.getLists();
  248 + }
182 249 }
183 250 // 响应结果
184   - String newContent = chunk.getResult() != null ? chunk.getResult().getOutput().getText() : null;
185   - newContent = StrUtil.nullToDefault(newContent, ""); // 避免 null 的 情况
186   - contentBuffer.append(newContent);
  251 + String newContent = AiUtils.getChatResponseContent(chunk);
  252 + String newReasoningContent = AiUtils.getChatResponseReasoningContent(chunk);
  253 + if (StrUtil.isNotEmpty(newContent)) {
  254 + contentBuffer.append(newContent);
  255 + }
  256 + if (StrUtil.isNotEmpty(newReasoningContent)) {
  257 + reasoningContentBuffer.append(newReasoningContent);
  258 + }
187 259 return success(new AiChatMessageSendRespVO()
188 260 .setSend(BeanUtils.toBean(userMessage, AiChatMessageSendRespVO.Message.class))
189 261 .setReceive(BeanUtils.toBean(assistantMessage, AiChatMessageSendRespVO.Message.class)
190   - .setContent(newContent).setSegments(segments)));
  262 + .setContent(StrUtil.nullToDefault(newContent, "")) // 避免 null 的 情况
  263 + .setReasoningContent(StrUtil.nullToDefault(newReasoningContent, "")) // 避免 null 的 情况
  264 + .setSegments(segments).setWebSearchPages(webSearchPages))); // 知识库 + 联网搜索
191 265 }).doOnComplete(() -> {
192 266 // 忽略租户,因为 Flux 异步无法透传租户
193 267 TenantUtils.executeIgnore(() -> chatMessageMapper.updateById(
194   - new AiChatMessageDO().setId(assistantMessage.getId()).setContent(contentBuffer.toString())));
  268 + new AiChatMessageDO().setId(assistantMessage.getId()).setContent(contentBuffer.toString())
  269 + .setReasoningContent(reasoningContentBuffer.toString())));
195 270 }).doOnError(throwable -> {
196 271 log.error("[sendChatMessageStream][userId({}) sendReqVO({}) 发生异常]", userId, sendReqVO, throwable);
197 272 // 忽略租户,因为 Flux 异步无法透传租户
198   - TenantUtils.executeIgnore(() -> chatMessageMapper.updateById(
199   - new AiChatMessageDO().setId(assistantMessage.getId()).setContent(throwable.getMessage())));
  273 + TenantUtils.executeIgnore(() -> {
  274 + // 如果有内容,则更新内容
  275 + if (StrUtil.isNotEmpty(contentBuffer)) {
  276 + chatMessageMapper.updateById(new AiChatMessageDO().setId(assistantMessage.getId())
  277 + .setContent(contentBuffer.toString()).setReasoningContent(reasoningContentBuffer.toString()));
  278 + } else {
  279 + // 否则,则进行删除
  280 + chatMessageMapper.deleteById(assistantMessage.getId());
  281 + }
  282 + });
  283 + }).doOnCancel(() -> {
  284 + log.info("[sendChatMessageStream][userId({}) sendReqVO({}) 取消请求]", userId, sendReqVO);
  285 + // 忽略租户,因为 Flux 异步无法透传租户
  286 + TenantUtils.executeIgnore(() -> {
  287 + // 如果有内容,则更新内容
  288 + if (StrUtil.isNotEmpty(contentBuffer)) {
  289 + chatMessageMapper.updateById(new AiChatMessageDO().setId(assistantMessage.getId())
  290 + .setContent(contentBuffer.toString()).setReasoningContent(reasoningContentBuffer.toString()));
  291 + } else {
  292 + // 否则,则进行删除
  293 + chatMessageMapper.deleteById(assistantMessage.getId());
  294 + }
  295 + });
200 296 }).onErrorResume(error -> Flux.just(error(ErrorCodeConstants.CHAT_STREAM_ERROR)));
201 297 }
202 298  
... ... @@ -211,7 +307,7 @@ public class AiChatMessageServiceImpl implements AiChatMessageService {
211 307 return Collections.emptyList();
212 308 }
213 309  
214   - // 2. 遍历找回
  310 + // 2. 遍历召回
215 311 List<AiKnowledgeSegmentSearchRespBO> knowledgeSegments = new ArrayList<>();
216 312 for (Long knowledgeId : role.getKnowledgeIds()) {
217 313 knowledgeSegments.addAll(knowledgeSegmentService.searchKnowledgeSegment(new AiKnowledgeSegmentSearchReqBO()
... ... @@ -222,6 +318,7 @@ public class AiChatMessageServiceImpl implements AiChatMessageService {
222 318  
223 319 private Prompt buildPrompt(AiChatConversationDO conversation, List<AiChatMessageDO> messages,
224 320 List<AiKnowledgeSegmentSearchRespBO> knowledgeSegments,
  321 + AiWebSearchResponse webSearchResponse,
225 322 AiModelDO model, AiChatMessageSendReqVO sendReqVO) {
226 323 List<Message> chatMessages = new ArrayList<>();
227 324 // 1.1 System Context 角色设定
... ... @@ -231,8 +328,14 @@ public class AiChatMessageServiceImpl implements AiChatMessageService {
231 328  
232 329 // 1.2 历史 history message 历史消息
233 330 List<AiChatMessageDO> contextMessages = filterContextMessages(messages, conversation, sendReqVO);
234   - contextMessages
235   - .forEach(message -> chatMessages.add(AiUtils.buildMessage(message.getType(), message.getContent())));
  331 + contextMessages.forEach(message -> {
  332 + chatMessages.add(AiUtils.buildMessage(message.getType(), message.getContent()));
  333 + UserMessage attachmentUserMessage = buildAttachmentUserMessage(message.getAttachmentUrls());
  334 + if (attachmentUserMessage != null) {
  335 + chatMessages.add(attachmentUserMessage);
  336 + }
  337 + // TODO @芋艿:历史的知识库;历史的搜索,要不要拼接?
  338 + });
236 339  
237 340 // 1.3 当前 user message 新发送消息
238 341 chatMessages.add(new UserMessage(sendReqVO.getContent()));
... ... @@ -245,23 +348,76 @@ public class AiChatMessageServiceImpl implements AiChatMessageService {
245 348 chatMessages.add(new UserMessage(String.format(KNOWLEDGE_USER_MESSAGE_TEMPLATE, reference)));
246 349 }
247 350  
248   - // 2.1 查询 tool 工具
249   - Set<String> toolNames = null;
250   - Map<String,Object> toolContext = Map.of();
251   - if (conversation.getRoleId() != null) {
252   - AiChatRoleDO chatRole = chatRoleService.getChatRole(conversation.getRoleId());
253   - if (chatRole != null && CollUtil.isNotEmpty(chatRole.getToolIds())) {
254   - toolNames = convertSet(toolService.getToolList(chatRole.getToolIds()), AiToolDO::getName);
255   - toolContext = AiUtils.buildCommonToolContext();
  351 + // 1.5 联网搜索,通过 UserMessage 实现
  352 + if (webSearchResponse != null && CollUtil.isNotEmpty(webSearchResponse.getLists())) {
  353 + String webSearch = webSearchResponse.getLists().stream()
  354 + .map(page -> {
  355 + String summary = StrUtil.isNotEmpty(page.getSummary()) ?
  356 + "\nSummary: " + page.getSummary() : "";
  357 + return "<WebSearch title=\"" + page.getTitle() + "\" url=\"" + page.getUrl() + "\">"
  358 + + StrUtil.blankToDefault(page.getSummary(), page.getSnippet()) + "</WebSearch>";
  359 + })
  360 + .collect(Collectors.joining("\n\n"));
  361 + chatMessages.add(new UserMessage(String.format(WEB_SEARCH_USER_MESSAGE_TEMPLATE, webSearch)));
  362 + }
  363 +
  364 + // 1.6 附件,通过 UserMessage 实现
  365 + if (CollUtil.isNotEmpty(sendReqVO.getAttachmentUrls())) {
  366 + UserMessage attachmentUserMessage = buildAttachmentUserMessage(sendReqVO.getAttachmentUrls());
  367 + if (attachmentUserMessage != null) {
  368 + chatMessages.add(attachmentUserMessage);
256 369 }
257 370 }
  371 +
  372 + // 2.1 查询 tool 工具
  373 + List<ToolCallback> toolCallbacks = getToolCallbackListByRoleId(conversation.getRoleId());
  374 + Map<String,Object> toolContext = CollUtil.isNotEmpty(toolCallbacks) ? AiUtils.buildCommonToolContext()
  375 + : Map.of();
258 376 // 2.2 构建 ChatOptions 对象
259 377 AiPlatformEnum platform = AiPlatformEnum.validatePlatform(model.getPlatform());
260 378 ChatOptions chatOptions = AiUtils.buildChatOptions(platform, model.getModel(),
261   - conversation.getTemperature(), conversation.getMaxTokens(), toolNames, toolContext);
  379 + conversation.getTemperature(), conversation.getMaxTokens(),
  380 + toolCallbacks, toolContext);
262 381 return new Prompt(chatMessages, chatOptions);
263 382 }
264 383  
  384 + private List<ToolCallback> getToolCallbackListByRoleId(Long roleId) {
  385 + if (roleId == null) {
  386 + return null;
  387 + }
  388 + AiChatRoleDO chatRole = chatRoleService.getChatRole(roleId);
  389 + if (chatRole == null) {
  390 + return null;
  391 + }
  392 + List<ToolCallback> toolCallbacks = new ArrayList<>();
  393 + // 1. 通过 toolIds
  394 + if (CollUtil.isNotEmpty(chatRole.getToolIds())) {
  395 + Set<String> toolNames = convertSet(toolService.getToolList(chatRole.getToolIds()), AiToolDO::getName);
  396 + toolNames.forEach(toolName -> {
  397 + ToolCallback toolCallback = toolCallbackResolver.resolve(toolName);
  398 + if (toolCallback != null) {
  399 + toolCallbacks.add(toolCallback);
  400 + }
  401 + });
  402 + }
  403 + // 2. 通过 mcpClients
  404 + if (CollUtil.isNotEmpty(mcpClients) && CollUtil.isNotEmpty(chatRole.getMcpClientNames())) {
  405 + chatRole.getMcpClientNames().forEach(mcpClientName -> {
  406 + // 2.1 标准化名字,参考 McpClientAutoConfiguration 的 connectedClientName 方法
  407 + String finalMcpClientName = mcpClientCommonProperties.getName() + " - " + mcpClientName;
  408 + // 2.2 匹配对应的 McpSyncClient
  409 + mcpClients.forEach(mcpClient -> {
  410 + if (ObjUtil.notEqual(mcpClient.getClientInfo().name(), finalMcpClientName)) {
  411 + return;
  412 + }
  413 + ToolCallback[] mcpToolCallBacks = new SyncMcpToolCallbackProvider(mcpClient).getToolCallbacks();
  414 + CollUtil.addAll(toolCallbacks, mcpToolCallBacks);
  415 + });
  416 + });
  417 + }
  418 + return toolCallbacks;
  419 + }
  420 +
265 421 /**
266 422 * 从历史消息中,获得倒序的 n 组消息作为消息上下文
267 423 * <p>
... ... @@ -302,14 +458,56 @@ public class AiChatMessageServiceImpl implements AiChatMessageService {
302 458 return contextMessages;
303 459 }
304 460  
  461 + private UserMessage buildAttachmentUserMessage(List<String> attachmentUrls) {
  462 + if (CollUtil.isEmpty(attachmentUrls)) {
  463 + return null;
  464 + }
  465 + // 读取文件内容
  466 + Map<String, String> attachmentContents = Maps.newLinkedHashMapWithExpectedSize(attachmentUrls.size());
  467 + for (String attachmentUrl : attachmentUrls) {
  468 + try {
  469 + String name = FileNameUtil.getName(attachmentUrl);
  470 + String mineType = FileTypeUtils.getMineType(name);
  471 + String content;
  472 + if (FileTypeUtils.isImage(mineType)) {
  473 + // 特殊:图片则转为 Base64
  474 + byte[] bytes = HttpUtil.downloadBytes(attachmentUrl);
  475 + content = Base64.encode(bytes);
  476 + } else {
  477 + content = knowledgeDocumentService.readUrl(attachmentUrl);
  478 + }
  479 + if (StrUtil.isNotEmpty(content)) {
  480 + attachmentContents.put(name, content);
  481 + }
  482 + } catch (Exception e) {
  483 + log.error("[buildAttachmentUserMessage][读取附件({}) 发生异常]", attachmentUrl, e);
  484 + }
  485 + }
  486 + if (CollUtil.isEmpty(attachmentContents)) {
  487 + return null;
  488 + }
  489 +
  490 + // 拼接 UserMessage 消息
  491 + String attachment = attachmentContents.entrySet().stream()
  492 + .map(entry -> "<Attachment name=\"" + entry.getKey() + "\">" + entry.getValue() + "</Attachment>")
  493 + .collect(Collectors.joining("\n\n"));
  494 + return new UserMessage(String.format(Attachment_USER_MESSAGE_TEMPLATE, attachment));
  495 + }
  496 +
305 497 private AiChatMessageDO createChatMessage(Long conversationId, Long replyId,
306   - AiModelDO model, Long userId, Long roleId,
307   - MessageType messageType, String content, Boolean useContext,
308   - List<AiKnowledgeSegmentSearchRespBO> knowledgeSegments) {
  498 + AiModelDO model, Long userId, Long roleId,
  499 + MessageType messageType, String content, Boolean useContext,
  500 + List<AiKnowledgeSegmentSearchRespBO> knowledgeSegments,
  501 + List<String> attachmentUrls,
  502 + AiWebSearchResponse webSearchResponse) {
309 503 AiChatMessageDO message = new AiChatMessageDO().setConversationId(conversationId).setReplyId(replyId)
310 504 .setModel(model.getModel()).setModelId(model.getId()).setUserId(userId).setRoleId(roleId)
311 505 .setType(messageType.getValue()).setContent(content).setUseContext(useContext)
312   - .setSegmentIds(convertList(knowledgeSegments, AiKnowledgeSegmentSearchRespBO::getId));
  506 + .setSegmentIds(convertList(knowledgeSegments, AiKnowledgeSegmentSearchRespBO::getId))
  507 + .setAttachmentUrls(attachmentUrls);
  508 + if (webSearchResponse != null) {
  509 + message.setWebSearchPages(webSearchResponse.getLists());
  510 + }
313 511 message.setCreateTime(LocalDateTime.now());
314 512 chatMessageMapper.insert(message);
315 513 return message;
... ...
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeSegmentServiceImpl.java
... ... @@ -18,6 +18,10 @@ import cn.iocoder.yudao.module.ai.dal.mysql.knowledge.AiKnowledgeSegmentMapper;
18 18 import cn.iocoder.yudao.module.ai.service.knowledge.bo.AiKnowledgeSegmentSearchReqBO;
19 19 import cn.iocoder.yudao.module.ai.service.knowledge.bo.AiKnowledgeSegmentSearchRespBO;
20 20 import cn.iocoder.yudao.module.ai.service.model.AiModelService;
  21 +import com.alibaba.cloud.ai.dashscope.rerank.DashScopeRerankOptions;
  22 +import com.alibaba.cloud.ai.model.RerankModel;
  23 +import com.alibaba.cloud.ai.model.RerankRequest;
  24 +import com.alibaba.cloud.ai.model.RerankResponse;
21 25 import jakarta.annotation.Resource;
22 26 import lombok.extern.slf4j.Slf4j;
23 27 import org.springframework.ai.document.Document;
... ... @@ -27,6 +31,7 @@ import org.springframework.ai.transformer.splitter.TokenTextSplitter;
27 31 import org.springframework.ai.vectorstore.SearchRequest;
28 32 import org.springframework.ai.vectorstore.VectorStore;
29 33 import org.springframework.ai.vectorstore.filter.FilterExpressionBuilder;
  34 +import org.springframework.beans.factory.annotation.Autowired;
30 35 import org.springframework.context.annotation.Lazy;
31 36 import org.springframework.stereotype.Service;
32 37  
... ... @@ -36,6 +41,7 @@ import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionU
36 41 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
37 42 import static cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants.KNOWLEDGE_SEGMENT_CONTENT_TOO_LONG;
38 43 import static cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants.KNOWLEDGE_SEGMENT_NOT_EXISTS;
  44 +import static org.springframework.ai.vectorstore.SearchRequest.SIMILARITY_THRESHOLD_ACCEPT_ALL;
39 45  
40 46 /**
41 47 * AI 知识库分片 Service 实现类
... ... @@ -55,6 +61,11 @@ public class AiKnowledgeSegmentServiceImpl implements AiKnowledgeSegmentService
55 61 VECTOR_STORE_METADATA_DOCUMENT_ID, String.class,
56 62 VECTOR_STORE_METADATA_SEGMENT_ID, String.class);
57 63  
  64 + /**
  65 + * Rerank 在向量检索时,检索数量 * 该系数,目的是为了提升 Rerank 的效果
  66 + */
  67 + private static final Integer RERANK_RETRIEVAL_FACTOR = 4;
  68 +
58 69 @Resource
59 70 private AiKnowledgeSegmentMapper segmentMapper;
60 71  
... ... @@ -69,6 +80,9 @@ public class AiKnowledgeSegmentServiceImpl implements AiKnowledgeSegmentService
69 80 @Resource
70 81 private TokenCountEstimator tokenCountEstimator;
71 82  
  83 + @Autowired(required = false) // 由于 spring.ai.model.rerank 配置项,可以关闭 RerankModel 的功能,所以这里只能不强制注入
  84 + private RerankModel rerankModel;
  85 +
72 86 @Override
73 87 public PageResult<AiKnowledgeSegmentDO> getKnowledgeSegmentPage(AiKnowledgeSegmentPageReqVO pageReqVO) {
74 88 return segmentMapper.selectPage(pageReqVO);
... ... @@ -211,28 +225,16 @@ public class AiKnowledgeSegmentServiceImpl implements AiKnowledgeSegmentService
211 225 // 1. 校验
212 226 AiKnowledgeDO knowledge = knowledgeService.validateKnowledgeExists(reqBO.getKnowledgeId());
213 227  
214   - // 2.1 向量检索
215   - VectorStore vectorStore = getVectorStoreById(knowledge);
216   - List<Document> documents = vectorStore.similaritySearch(SearchRequest.builder()
217   - .query(reqBO.getContent())
218   - .topK(ObjUtil.defaultIfNull(reqBO.getTopK(), knowledge.getTopK()))
219   - .similarityThreshold(
220   - ObjUtil.defaultIfNull(reqBO.getSimilarityThreshold(), knowledge.getSimilarityThreshold()))
221   - .filterExpression(new FilterExpressionBuilder()
222   - .eq(VECTOR_STORE_METADATA_KNOWLEDGE_ID, reqBO.getKnowledgeId().toString())
223   - .build())
224   - .build());
225   - if (CollUtil.isEmpty(documents)) {
226   - return ListUtil.empty();
227   - }
228   - // 2.2 段落召回
  228 + // 2. 检索
  229 + List<Document> documents = searchDocument(knowledge, reqBO);
  230 +
  231 + // 3.1 段落召回
229 232 List<AiKnowledgeSegmentDO> segments = segmentMapper
230 233 .selectListByVectorIds(convertList(documents, Document::getId));
231 234 if (CollUtil.isEmpty(segments)) {
232 235 return ListUtil.empty();
233 236 }
234   -
235   - // 3. 增加召回次数
  237 + // 3.2 增加召回次数
236 238 segmentMapper.updateRetrievalCountIncrByIds(convertList(segments, AiKnowledgeSegmentDO::getId));
237 239  
238 240 // 4. 构建结果
... ... @@ -249,6 +251,42 @@ public class AiKnowledgeSegmentServiceImpl implements AiKnowledgeSegmentService
249 251 return result;
250 252 }
251 253  
  254 + /**
  255 + * 基于 Embedding + Rerank Model,检索知识库中的文档
  256 + *
  257 + * @param knowledge 知识库
  258 + * @param reqBO 检索请求
  259 + * @return 文档列表
  260 + */
  261 + private List<Document> searchDocument(AiKnowledgeDO knowledge, AiKnowledgeSegmentSearchReqBO reqBO) {
  262 + VectorStore vectorStore = getVectorStoreById(knowledge);
  263 + Integer topK = ObjUtil.defaultIfNull(reqBO.getTopK(), knowledge.getTopK());
  264 + Double similarityThreshold = ObjUtil.defaultIfNull(reqBO.getSimilarityThreshold(), knowledge.getSimilarityThreshold());
  265 +
  266 + // 1. 向量检索
  267 + int searchTopK = rerankModel != null ? topK * RERANK_RETRIEVAL_FACTOR : topK;
  268 + double searchSimilarityThreshold = rerankModel != null ? SIMILARITY_THRESHOLD_ACCEPT_ALL : similarityThreshold;
  269 + SearchRequest.Builder searchRequestBuilder = SearchRequest.builder()
  270 + .query(reqBO.getContent())
  271 + .topK(searchTopK).similarityThreshold(searchSimilarityThreshold)
  272 + .filterExpression(new FilterExpressionBuilder()
  273 + .eq(VECTOR_STORE_METADATA_KNOWLEDGE_ID, reqBO.getKnowledgeId().toString()).build());
  274 + List<Document> documents = vectorStore.similaritySearch(searchRequestBuilder.build());
  275 + if (CollUtil.isEmpty(documents)) {
  276 + return documents;
  277 + }
  278 +
  279 + // 2. Rerank 重排序
  280 + if (rerankModel != null) {
  281 + RerankResponse rerankResponse = rerankModel.call(new RerankRequest(reqBO.getContent(), documents,
  282 + DashScopeRerankOptions.builder().withTopN(topK).build()));
  283 + documents = convertList(rerankResponse.getResults(),
  284 + documentWithScore -> documentWithScore.getScore() >= similarityThreshold
  285 + ? documentWithScore.getOutput() : null);
  286 + }
  287 + return documents;
  288 + }
  289 +
252 290 @Override
253 291 public List<AiKnowledgeSegmentDO> splitContent(String url, Integer segmentMaxTokens) {
254 292 // 1. 读取 URL 内容
... ...
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiModelServiceImpl.java
1 1 package cn.iocoder.yudao.module.ai.service.model;
2 2  
3 3 import cn.iocoder.yudao.module.ai.enums.model.AiPlatformEnum;
4   -import cn.iocoder.yudao.module.ai.framework.ai.core.AiModelFactory;
  4 +import cn.iocoder.yudao.module.ai.framework.ai.core.model.AiModelFactory;
5 5 import cn.iocoder.yudao.module.ai.framework.ai.core.model.midjourney.api.MidjourneyApi;
6 6 import cn.iocoder.yudao.module.ai.framework.ai.core.model.suno.api.SunoApi;
7 7 import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
... ...
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiToolServiceImpl.java
1 1 package cn.iocoder.yudao.module.ai.service.model;
2 2  
3   -import cn.hutool.extra.spring.SpringUtil;
4 3 import cn.iocoder.yudao.framework.common.pojo.PageResult;
5 4 import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
6 5 import cn.iocoder.yudao.module.ai.controller.admin.model.vo.tool.AiToolPageReqVO;
... ... @@ -8,7 +7,8 @@ import cn.iocoder.yudao.module.ai.controller.admin.model.vo.tool.AiToolSaveReqVO
8 7 import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiToolDO;
9 8 import cn.iocoder.yudao.module.ai.dal.mysql.model.AiToolMapper;
10 9 import jakarta.annotation.Resource;
11   -import org.springframework.beans.factory.NoSuchBeanDefinitionException;
  10 +import org.springframework.ai.tool.ToolCallback;
  11 +import org.springframework.ai.tool.resolution.ToolCallbackResolver;
12 12 import org.springframework.stereotype.Service;
13 13 import org.springframework.validation.annotation.Validated;
14 14  
... ... @@ -31,6 +31,9 @@ public class AiToolServiceImpl implements AiToolService {
31 31 @Resource
32 32 private AiToolMapper toolMapper;
33 33  
  34 + @Resource
  35 + private ToolCallbackResolver toolCallbackResolver;
  36 +
34 37 @Override
35 38 public Long createTool(AiToolSaveReqVO createReqVO) {
36 39 // 校验名称是否存在
... ... @@ -70,9 +73,8 @@ public class AiToolServiceImpl implements AiToolService {
70 73 }
71 74  
72 75 private void validateToolNameExists(String name) {
73   - try {
74   - SpringUtil.getBean(name);
75   - } catch (NoSuchBeanDefinitionException e) {
  76 + ToolCallback toolCallback = toolCallbackResolver.resolve(name);
  77 + if (toolCallback == null) {
76 78 throw exception(TOOL_NAME_NOT_EXISTS, name);
77 79 }
78 80 }
... ...
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/service/model/tool/DirectoryListToolFunction.java renamed to yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/tool/function/DirectoryListToolFunction.java
1   -package cn.iocoder.yudao.module.ai.service.model.tool;
  1 +package cn.iocoder.yudao.module.ai.tool.function;
2 2  
3 3 import cn.hutool.core.date.LocalDateTimeUtil;
4 4 import cn.hutool.core.io.FileUtil;
... ...
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/service/model/tool/UserProfileQueryToolFunction.java renamed to yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/tool/function/UserProfileQueryToolFunction.java
1   -package cn.iocoder.yudao.module.ai.service.model.tool;
  1 +package cn.iocoder.yudao.module.ai.tool.function;
2 2  
3 3 import cn.iocoder.yudao.module.ai.util.AiUtils;
4 4 import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
... ...
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/service/model/tool/WeatherQueryToolFunction.java renamed to yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/tool/function/WeatherQueryToolFunction.java
1   -package cn.iocoder.yudao.module.ai.service.model.tool;
  1 +package cn.iocoder.yudao.module.ai.tool.function;
2 2  
3 3 import cn.hutool.core.date.LocalDateTimeUtil;
4 4 import cn.hutool.core.util.RandomUtil;
... ...
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/tool/function/package-info.java 0 → 100644
  1 +/**
  2 + * 参考 <a href="https://docs.spring.io/spring-ai/reference/api/tools.html#_methods_as_tools">Tool Calling —— Methods as Tools</a>
  3 + */
  4 +package cn.iocoder.yudao.module.ai.tool.function;
0 5 \ No newline at end of file
... ...
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/tool/method/Person.java 0 → 100644
  1 +package cn.iocoder.yudao.module.ai.tool.method;
  2 +
  3 +/**
  4 + * 来自 Spring AI 官方文档
  5 + *
  6 + * Represents a person with basic information.
  7 + * This is an immutable record.
  8 + */
  9 +public record Person(
  10 + int id,
  11 + String firstName,
  12 + String lastName,
  13 + String email,
  14 + String sex,
  15 + String ipAddress,
  16 + String jobTitle,
  17 + int age
  18 +) {
  19 +}
0 20 \ No newline at end of file
... ...
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/tool/method/PersonService.java 0 → 100644
  1 +package cn.iocoder.yudao.module.ai.tool.method;
  2 +
  3 +import java.util.List;
  4 +import java.util.Optional;
  5 +
  6 +/**
  7 + * 来自 Spring AI 官方文档
  8 + *
  9 + * Service interface for managing Person data.
  10 + * Defines the contract for CRUD operations and search/filter functionalities.
  11 + */
  12 +public interface PersonService {
  13 +
  14 + /**
  15 + * Creates a new Person record.
  16 + * Assigns a unique ID to the person and stores it.
  17 + *
  18 + * @param personData The data for the new person (ID field is ignored). Must not be null.
  19 + * @return The created Person record, including the generated ID.
  20 + */
  21 + Person createPerson(Person personData);
  22 +
  23 + /**
  24 + * Retrieves a Person by their unique ID.
  25 + *
  26 + * @param id The ID of the person to retrieve.
  27 + * @return An Optional containing the found Person, or an empty Optional if not found.
  28 + */
  29 + Optional<Person> getPersonById(int id);
  30 +
  31 + /**
  32 + * Retrieves all Person records currently stored.
  33 + *
  34 + * @return An unmodifiable List containing all Persons. Returns an empty list if none exist.
  35 + */
  36 + List<Person> getAllPersons();
  37 +
  38 + /**
  39 + * Updates an existing Person record identified by ID.
  40 + * Replaces the existing data with the provided data, keeping the original ID.
  41 + *
  42 + * @param id The ID of the person to update.
  43 + * @param updatedPersonData The new data for the person (ID field is ignored). Must not be null.
  44 + * @return true if the person was found and updated, false otherwise.
  45 + */
  46 + boolean updatePerson(int id, Person updatedPersonData);
  47 +
  48 + /**
  49 + * Deletes a Person record identified by ID.
  50 + *
  51 + * @param id The ID of the person to delete.
  52 + * @return true if the person was found and deleted, false otherwise.
  53 + */
  54 + boolean deletePerson(int id);
  55 +
  56 + /**
  57 + * Searches for Persons whose job title contains the given query string (case-insensitive).
  58 + *
  59 + * @param jobTitleQuery The string to search for within job titles. Can be null or blank.
  60 + * @return An unmodifiable List of matching Persons. Returns an empty list if no matches or query is invalid.
  61 + */
  62 + List<Person> searchByJobTitle(String jobTitleQuery);
  63 +
  64 + /**
  65 + * Filters Persons by their exact sex (case-insensitive).
  66 + *
  67 + * @param sex The sex to filter by (e.g., "Male", "Female"). Can be null or blank.
  68 + * @return An unmodifiable List of matching Persons. Returns an empty list if no matches or filter is invalid.
  69 + */
  70 + List<Person> filterBySex(String sex);
  71 +
  72 + /**
  73 + * Filters Persons by their exact age.
  74 + *
  75 + * @param age The age to filter by.
  76 + * @return An unmodifiable List of matching Persons. Returns an empty list if no matches.
  77 + */
  78 + List<Person> filterByAge(int age);
  79 +
  80 +}
0 81 \ No newline at end of file
... ...
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/tool/method/PersonServiceImpl.java 0 → 100644
  1 +package cn.iocoder.yudao.module.ai.tool.method;
  2 +
  3 +import jakarta.annotation.PostConstruct;
  4 +import lombok.extern.slf4j.Slf4j;
  5 +import org.springframework.ai.tool.annotation.Tool;
  6 +import org.springframework.stereotype.Service;
  7 +
  8 +import java.util.Collections;
  9 +import java.util.List;
  10 +import java.util.Map;
  11 +import java.util.Optional;
  12 +import java.util.concurrent.ConcurrentHashMap;
  13 +import java.util.concurrent.atomic.AtomicInteger;
  14 +import java.util.stream.Collectors;
  15 +import java.util.stream.Stream;
  16 +
  17 +/**
  18 + * 来自 Spring AI 官方文档
  19 + *
  20 + * Implementation of the PersonService interface using an in-memory data store.
  21 + * Manages a collection of Person objects loaded from embedded CSV data.
  22 + * This class is thread-safe due to the use of ConcurrentHashMap and AtomicInteger.
  23 + */
  24 +@Service
  25 +@Slf4j
  26 +public class PersonServiceImpl implements PersonService {
  27 +
  28 + private final Map<Integer, Person> personStore = new ConcurrentHashMap<>();
  29 +
  30 + private AtomicInteger idGenerator;
  31 +
  32 + /**
  33 + * Embedded CSV data for initial population
  34 + */
  35 + private static final String CSV_DATA = """
  36 + Id,FirstName,LastName,Email,Sex,IpAddress,JobTitle,Age
  37 + 1,Fons,Tollfree,ftollfree0@senate.gov,Male,55.1 Tollfree Lane,Research Associate,31
  38 + 2,Emlynne,Tabourier,etabourier1@networksolutions.com,Female,18 Tabourier Way,Associate Professor,38
  39 + 3,Shae,Johncey,sjohncey2@yellowpages.com,Male,1 Johncey Circle,Structural Analysis Engineer,30
  40 + 4,Sebastien,Bradly,sbradly3@mapquest.com,Male,2 Bradly Hill,Chief Executive Officer,40
  41 + 5,Harriott,Kitteringham,hkitteringham4@typepad.com,Female,3 Kitteringham Drive,VP Sales,47
  42 + 6,Anallise,Parradine,aparradine5@miibeian.gov.cn,Female,4 Parradine Street,Analog Circuit Design manager,44
  43 + 7,Gorden,Kirkbright,gkirkbright6@reuters.com,Male,5 Kirkbright Plaza,Senior Editor,40
  44 + 8,Veradis,Ledwitch,vledwitch7@google.com.au,Female,6 Ledwitch Avenue,Computer Systems Analyst IV,44
  45 + 9,Agnesse,Penhalurick,apenhalurick8@google.it,Female,7 Penhalurick Terrace,Automation Specialist IV,41
  46 + 10,Bibby,Hutable,bhutable9@craigslist.org,Female,8 Hutable Place,Account Representative I,43
  47 + 11,Karoly,Lightoller,klightollera@rakuten.co.jp,Female,9 Lightoller Parkway,Senior Developer,46
  48 + 12,Cristine,Durrad,cdurradb@aol.com,Female,10 Durrad Center,Senior Developer,48
  49 + 13,Aggy,Napier,anapierc@hostgator.com,Female,11 Napier Court,VP Product Management,44
  50 + 14,Prisca,Caddens,pcaddensd@vinaora.com,Female,12 Caddens Alley,Business Systems Development Analyst,41
  51 + 15,Khalil,McKernan,kmckernane@google.fr,Male,13 McKernan Pass,Engineer IV,44
  52 + 16,Lorry,MacTrusty,lmactrustyf@eventbrite.com,Male,14 MacTrusty Junction,Design Engineer,42
  53 + 17,Casandra,Worsell,cworsellg@goo.gl,Female,15 Worsell Point,Systems Administrator IV,45
  54 + 18,Ulrikaumeko,Haveline,uhavelineh@usgs.gov,Female,16 Haveline Trail,Financial Advisor,42
  55 + 19,Shurlocke,Albany,salbanyi@artisteer.com,Male,17 Albany Plaza,Software Test Engineer III,46
  56 + 20,Myrilla,Brimilcombe,mbrimilcombej@accuweather.com,Female,18 Brimilcombe Road,Programmer Analyst I,48
  57 + 21,Carlina,Scimonelli,cscimonellik@va.gov,Female,19 Scimonelli Pass,Help Desk Technician,45
  58 + 22,Tina,Goullee,tgoulleel@miibeian.gov.cn,Female,20 Goullee Crossing,Accountant IV,43
  59 + 23,Adriaens,Storek,astorekm@devhub.com,Female,21 Storek Avenue,Recruiting Manager,40
  60 + 24,Tedra,Giraudot,tgiraudotn@wiley.com,Female,22 Giraudot Terrace,Speech Pathologist,47
  61 + 25,Josiah,Soares,jsoareso@google.nl,Male,23 Soares Street,Tax Accountant,45
  62 + 26,Kayle,Gaukrodge,kgaukrodgep@wikispaces.com,Female,24 Gaukrodge Parkway,Accountant II,43
  63 + 27,Ardys,Chuter,achuterq@ustream.tv,Female,25 Chuter Drive,Engineer IV,41
  64 + 28,Francyne,Baudinet,fbaudinetr@newyorker.com,Female,26 Baudinet Center,VP Accounting,48
  65 + 29,Gerick,Bullan,gbullans@seesaa.net,Male,27 Bullan Way,Senior Financial Analyst,43
  66 + 30,Northrup,Grivori,ngrivorit@unc.edu,Male,28 Grivori Plaza,Systems Administrator I,45
  67 + 31,Town,Duguid,tduguidu@squarespace.com,Male,29 Duguid Pass,Safety Technician IV,46
  68 + 32,Pierette,Kopisch,pkopischv@google.com.br,Female,30 Kopisch Lane,Director of Sales,41
  69 + 33,Jacquenetta,Le Prevost,jleprevostw@netlog.com,Female,31 Le Prevost Trail,Senior Developer,47
  70 + 34,Garvy,Rusted,grustedx@aboutads.info,Male,32 Rusted Junction,Senior Developer,42
  71 + 35,Clarice,Aysh,cayshy@merriam-webster.com,Female,33 Aysh Avenue,VP Quality Control,40
  72 + 36,Tracie,Fedorski,tfedorskiz@bloglines.com,Male,34 Fedorski Terrace,Design Engineer,44
  73 + 37,Noelyn,Matushenko,nmatushenko10@globo.com,Female,35 Matushenko Place,VP Sales,48
  74 + 38,Rudiger,Klaesson,rklaesson11@usnews.com,Male,36 Klaesson Road,Database Administrator IV,43
  75 + 39,Mirella,Syddie,msyddie12@geocities.jp,Female,37 Syddie Circle,Geological Engineer,46
  76 + 40,Donalt,O'Lunny,dolunny13@elpais.com,Male,38 O'Lunny Center,Analog Circuit Design manager,41
  77 + 41,Guntar,Deniskevich,gdeniskevich14@google.com.hk,Male,39 Deniskevich Way,Structural Engineer,47
  78 + 42,Hort,Shufflebotham,hshufflebotham15@about.me,Male,40 Shufflebotham Court,Structural Analysis Engineer,45
  79 + 43,Dominique,Thickett,dthickett16@slashdot.org,Male,41 Thickett Crossing,Safety Technician I,42
  80 + 44,Zebulen,Piscopello,zpiscopello17@umich.edu,Male,42 Piscopello Parkway,Web Developer II,40
  81 + 45,Mellicent,Mac Giany,mmacgiany18@state.tx.us,Female,43 Mac Giany Pass,Assistant Manager,44
  82 + 46,Merle,Bounds,mbounds19@amazon.co.jp,Female,44 Bounds Alley,Systems Administrator III,41
  83 + 47,Madelle,Farbrace,mfarbrace1a@xinhuanet.com,Female,45 Farbrace Terrace,Quality Engineer,48
  84 + 48,Galvin,O'Sheeryne,gosheeryne1b@addtoany.com,Male,46 O'Sheeryne Way,Environmental Specialist,43
  85 + 49,Guillemette,Bootherstone,gbootherstone1c@nationalgeographic.com,Female,47 Bootherstone Plaza,Professor,46
  86 + 50,Letti,Aylmore,laylmore1d@vinaora.com,Female,48 Aylmore Circle,Automation Specialist I,40
  87 + 51,Nonie,Rivalland,nrivalland1e@weather.com,Female,49 Rivalland Avenue,Software Test Engineer IV,45
  88 + 52,Jacquelynn,Halfacre,jhalfacre1f@surveymonkey.com,Female,50 Halfacre Pass,Geologist II,42
  89 + 53,Anderea,MacKibbon,amackibbon1g@weibo.com,Female,51 MacKibbon Parkway,Automation Specialist II,47
  90 + 54,Wash,Klimko,wklimko1h@slashdot.org,Male,52 Klimko Alley,Database Administrator I,40
  91 + 55,Flori,Kynett,fkynett1i@auda.org.au,Female,53 Kynett Trail,Quality Control Specialist,46
  92 + 56,Libbey,Penswick,lpenswick1j@google.co.uk,Female,54 Penswick Point,VP Accounting,43
  93 + 57,Silvanus,Skellorne,sskellorne1k@booking.com,Male,55 Skellorne Drive,Account Executive,48
  94 + 58,Carmine,Mateos,cmateos1l@plala.or.jp,Male,56 Mateos Terrace,Systems Administrator I,41
  95 + 59,Sheffie,Blazewicz,sblazewicz1m@google.com.au,Male,57 Blazewicz Center,VP Sales,44
  96 + 60,Leanor,Worsnop,lworsnop1n@uol.com.br,Female,58 Worsnop Plaza,Systems Administrator III,45
  97 + 61,Caspar,Pamment,cpamment1o@google.co.jp,Male,59 Pamment Court,Senior Financial Analyst,42
  98 + 62,Justinian,Pentycost,jpentycost1p@sciencedaily.com,Male,60 Pentycost Way,Senior Quality Engineer,47
  99 + 63,Gerianne,Jarnell,gjarnell1q@bing.com,Female,61 Jarnell Avenue,Help Desk Operator,40
  100 + 64,Boycie,Zanetto,bzanetto1r@about.com,Male,62 Zanetto Place,Quality Engineer,46
  101 + 65,Camilla,Mac Giany,cmacgiany1s@state.gov,Female,63 Mac Giany Parkway,Senior Cost Accountant,43
  102 + 66,Hadlee,Piscopiello,hpiscopiello1t@artisteer.com,Male,64 Piscopiello Street,Account Representative III,48
  103 + 67,Bobbie,Penvarden,bpenvarden1u@google.cn,Male,65 Penvarden Lane,Help Desk Operator,41
  104 + 68,Ali,Gowlett,agowlett1v@parallels.com,Male,66 Gowlett Pass,VP Marketing,44
  105 + 69,Olivette,Acome,oacome1w@qq.com,Female,67 Acome Hill,VP Product Management,45
  106 + 70,Jehanna,Brotherheed,jbrotherheed1x@google.nl,Female,68 Brotherheed Junction,Database Administrator III,42
  107 + 71,Morgan,Berthomieu,mberthomieu1y@artisteer.com,Male,69 Berthomieu Alley,Systems Administrator II,47
  108 + 72,Linzy,Shilladay,lshilladay1z@icq.com,Female,70 Shilladay Trail,Research Assistant IV,40
  109 + 73,Faydra,Brimner,fbrimner20@mozilla.org,Female,71 Brimner Road,Senior Editor,46
  110 + 74,Gwenore,Oxlee,goxlee21@devhub.com,Female,72 Oxlee Terrace,Systems Administrator II,43
  111 + 75,Evangelin,Beinke,ebeinke22@mozilla.com,Female,73 Beinke Circle,Accountant I,48
  112 + 76,Missy,Cockling,mcockling23@si.edu,Female,74 Cockling Way,Software Engineer I,41
  113 + 77,Suzanne,Klimschak,sklimschak24@etsy.com,Female,75 Klimschak Plaza,Tax Accountant,44
  114 + 78,Candide,Goricke,cgoricke25@weebly.com,Female,76 Goricke Pass,Sales Associate,45
  115 + 79,Gerome,Pinsent,gpinsent26@google.com.au,Male,77 Pinsent Junction,Software Consultant,42
  116 + 80,Lezley,Mac Giany,lmacgiany27@scribd.com,Male,78 Mac Giany Alley,Operator,47
  117 + 81,Tobiah,Durn,tdurn28@state.tx.us,Male,79 Durn Court,VP Sales,40
  118 + 82,Sherlocke,Cockshoot,scockshoot29@yelp.com,Male,80 Cockshoot Street,Senior Financial Analyst,46
  119 + 83,Myrle,Speenden,mspeenden2a@utexas.edu,Female,81 Speenden Center,Senior Developer,43
  120 + 84,Isidore,Gorries,igorries2b@flavors.me,Male,82 Gorries Parkway,Sales Representative,48
  121 + 85,Isac,Kitchingman,ikitchingman2c@businessinsider.com,Male,83 Kitchingman Drive,VP Accounting,41
  122 + 86,Benedetta,Purrier,bpurrier2d@admin.ch,Female,84 Purrier Trail,VP Accounting,44
  123 + 87,Tera,Fitchell,tfitchell2e@fotki.com,Female,85 Fitchell Place,Software Engineer IV,45
  124 + 88,Abbe,Pamment,apamment2f@about.com,Male,86 Pamment Avenue,VP Sales,42
  125 + 89,Jandy,Gommowe,jgommowe2g@angelfire.com,Female,87 Gommowe Road,Financial Analyst,47
  126 + 90,Karena,Fussey,kfussey2h@google.com.au,Female,88 Fussey Point,Assistant Professor,40
  127 + 91,Gaspar,Pammenter,gpammenter2i@google.com.br,Male,89 Pammenter Hill,Help Desk Operator,46
  128 + 92,Stanwood,Mac Giany,smacgiany2j@prlog.org,Male,90 Mac Giany Terrace,Research Associate,43
  129 + 93,Byrom,Beedell,bbeedell2k@google.co.jp,Male,91 Beedell Way,VP Sales,48
  130 + 94,Annabella,Rowbottom,arowbottom2l@google.com.au,Female,92 Rowbottom Plaza,Help Desk Operator,41
  131 + 95,Rodolphe,Debell,rdebell2m@imageshack.us,Male,93 Debell Pass,Design Engineer,44
  132 + 96,Tyne,Gommey,tgommey2n@joomla.org,Female,94 Gommey Junction,VP Marketing,45
  133 + 97,Christoper,Pincked,cpincked2o@icq.com,Male,95 Pincked Alley,Human Resources Manager,42
  134 + 98,Kore,Le Prevost,kleprevost2p@tripadvisor.com,Female,96 Le Prevost Street,VP Quality Control,47
  135 + 99,Ceciley,Petrolli,cpetrolli2q@oaic.gov.au,Female,97 Petrolli Court,Senior Developer,40
  136 + 100,Elspeth,Mac Giany,emacgiany2r@icio.us,Female,98 Mac Giany Parkway,Internal Auditor,46
  137 + """;
  138 +
  139 + /**
  140 + * Initializes the service after dependency injection by loading data from the CSV string.
  141 + * Uses @PostConstruct to ensure this runs after the bean is created.
  142 + */
  143 + @PostConstruct
  144 + private void initializeData() {
  145 + log.info("Initializing PersonService data store...");
  146 + int maxId = loadDataFromCsv();
  147 + idGenerator = new AtomicInteger(maxId);
  148 + log.info("PersonService initialized with {} records. Next ID: {}", personStore.size(), idGenerator.get() + 1);
  149 + }
  150 +
  151 + /**
  152 + * Parses the embedded CSV data and populates the in-memory store.
  153 + * Calculates the maximum ID found in the data to initialize the ID generator.
  154 + *
  155 + * @return The maximum ID found in the loaded CSV data.
  156 + */
  157 + private int loadDataFromCsv() {
  158 + final AtomicInteger currentMaxId = new AtomicInteger(0);
  159 + // Clear existing data before loading (important for tests or re-initialization scenarios)
  160 + personStore.clear();
  161 + try (Stream<String> lines = CSV_DATA.lines().skip(1)) { // Skip header row
  162 + lines.forEach(line -> {
  163 + try {
  164 + // Split carefully, handling potential commas within quoted fields if necessary (simple split here)
  165 + String[] fields = line.split(",", 8); // Limit split to handle potential commas in job title
  166 + if (fields.length == 8) {
  167 + int id = Integer.parseInt(fields[0].trim());
  168 + String firstName = fields[1].trim();
  169 + String lastName = fields[2].trim();
  170 + String email = fields[3].trim();
  171 + String sex = fields[4].trim();
  172 + String ipAddress = fields[5].trim();
  173 + String jobTitle = fields[6].trim();
  174 + int age = Integer.parseInt(fields[7].trim());
  175 +
  176 + Person person = new Person(id, firstName, lastName, email, sex, ipAddress, jobTitle, age);
  177 + personStore.put(id, person);
  178 + currentMaxId.updateAndGet(max -> Math.max(max, id));
  179 + } else {
  180 + log.warn("Skipping malformed CSV line (expected 8 fields, found {}): {}", fields.length, line);
  181 + }
  182 + } catch (NumberFormatException e) {
  183 + log.warn("Skipping line due to parsing error (ID or Age): {} - Error: {}", line, e.getMessage());
  184 + } catch (Exception e) {
  185 + log.error("Skipping line due to unexpected error: {} - Error: {}", line, e.getMessage(), e);
  186 + }
  187 + });
  188 + } catch (Exception e) {
  189 + log.error("Fatal error reading embedded CSV data: {}", e.getMessage(), e);
  190 + // In a real application, might throw a specific initialization exception
  191 + }
  192 + return currentMaxId.get();
  193 + }
  194 +
  195 + @Override
  196 + @Tool(
  197 + name = "ps_create_person",
  198 + description = "Create a new person record in the in-memory store."
  199 + )
  200 + public Person createPerson(Person personData) {
  201 + if (personData == null) {
  202 + throw new IllegalArgumentException("Person data cannot be null");
  203 + }
  204 + int newId = idGenerator.incrementAndGet();
  205 + // Create a new Person record using data from the input, but with the generated ID
  206 + Person newPerson = new Person(
  207 + newId,
  208 + personData.firstName(),
  209 + personData.lastName(),
  210 + personData.email(),
  211 + personData.sex(),
  212 + personData.ipAddress(),
  213 + personData.jobTitle(),
  214 + personData.age()
  215 + );
  216 + personStore.put(newId, newPerson);
  217 + log.debug("Created person: {}", newPerson);
  218 + return newPerson;
  219 + }
  220 +
  221 + @Override
  222 + @Tool(
  223 + name = "ps_get_person_by_id",
  224 + description = "Retrieve a person record by ID from the in-memory store."
  225 + )
  226 + public Optional<Person> getPersonById(int id) {
  227 + Person person = personStore.get(id);
  228 + log.debug("Retrieved person by ID {}: {}", id, person);
  229 + return Optional.ofNullable(person);
  230 + }
  231 +
  232 + @Override
  233 + @Tool(
  234 + name = "ps_get_all_persons",
  235 + description = "Retrieve all person records from the in-memory store."
  236 + )
  237 + public List<Person> getAllPersons() {
  238 + // Return an unmodifiable view of the values
  239 + List<Person> allPersons = personStore.values().stream().toList();
  240 + log.debug("Retrieved all persons (count: {})", allPersons.size());
  241 + return allPersons;
  242 + }
  243 +
  244 + @Override
  245 + @Tool(
  246 + name = "ps_update_person",
  247 + description = "Update an existing person record by ID in the in-memory store."
  248 + )
  249 + public boolean updatePerson(int id, Person updatedPersonData) {
  250 + if (updatedPersonData == null) {
  251 + throw new IllegalArgumentException("Updated person data cannot be null");
  252 + }
  253 + // Use computeIfPresent for atomic update if the key exists
  254 + Person result = personStore.computeIfPresent(id, (key, existingPerson) ->
  255 + // Create a new Person record with the original ID but updated data
  256 + new Person(
  257 + id, // Keep original ID
  258 + updatedPersonData.firstName(),
  259 + updatedPersonData.lastName(),
  260 + updatedPersonData.email(),
  261 + updatedPersonData.sex(),
  262 + updatedPersonData.ipAddress(),
  263 + updatedPersonData.jobTitle(),
  264 + updatedPersonData.age()
  265 + )
  266 + );
  267 + boolean updated = result != null;
  268 + log.debug("Update attempt for ID {}: {}", id, updated ? "Successful" : "Failed (Not Found)");
  269 + if(updated) log.trace("Updated person data for ID {}: {}", id, result);
  270 + return updated;
  271 + }
  272 +
  273 + @Override
  274 + @Tool(
  275 + name = "ps_delete_person",
  276 + description = "Delete a person record by ID from the in-memory store."
  277 + )
  278 + public boolean deletePerson(int id) {
  279 + boolean removed = personStore.remove(id) != null;
  280 + log.debug("Delete attempt for ID {}: {}", id, removed ? "Successful" : "Failed (Not Found)");
  281 + return removed;
  282 + }
  283 +
  284 + @Override
  285 + @Tool(
  286 + name = "ps_search_by_job_title",
  287 + description = "Search for persons by job title in the in-memory store."
  288 + )
  289 + public List<Person> searchByJobTitle(String jobTitleQuery) {
  290 + if (jobTitleQuery == null || jobTitleQuery.isBlank()) {
  291 + log.debug("Search by job title skipped due to blank query.");
  292 + return Collections.emptyList();
  293 + }
  294 + String lowerCaseQuery = jobTitleQuery.toLowerCase();
  295 + List<Person> results = personStore.values().stream()
  296 + .filter(person -> person.jobTitle() != null && person.jobTitle().toLowerCase().contains(lowerCaseQuery))
  297 + .collect(Collectors.toList());
  298 + log.debug("Search by job title '{}' found {} results.", jobTitleQuery, results.size());
  299 + return Collections.unmodifiableList(results);
  300 + }
  301 +
  302 + @Override
  303 + @Tool(
  304 + name = "ps_filter_by_sex",
  305 + description = "Filters Persons by sex (case-insensitive)."
  306 + )
  307 + public List<Person> filterBySex(String sex) {
  308 + if (sex == null || sex.isBlank()) {
  309 + log.debug("Filter by sex skipped due to blank filter.");
  310 + return Collections.emptyList();
  311 + }
  312 + List<Person> results = personStore.values().stream()
  313 + .filter(person -> person.sex() != null && person.sex().equalsIgnoreCase(sex))
  314 + .collect(Collectors.toList());
  315 + log.debug("Filter by sex '{}' found {} results.", sex, results.size());
  316 + return Collections.unmodifiableList(results);
  317 + }
  318 +
  319 + @Override
  320 + @Tool(
  321 + name = "ps_filter_by_age",
  322 + description = "Filters Persons by age."
  323 + )
  324 + public List<Person> filterByAge(int age) {
  325 + if (age < 0) {
  326 + log.debug("Filter by age skipped due to negative age: {}", age);
  327 + return Collections.emptyList(); // Or throw IllegalArgumentException based on requirements
  328 + }
  329 + List<Person> results = personStore.values().stream()
  330 + .filter(person -> person.age() == age)
  331 + .collect(Collectors.toList());
  332 + log.debug("Filter by age {} found {} results.", age, results.size());
  333 + return Collections.unmodifiableList(results);
  334 + }
  335 +
  336 +}
0 337 \ No newline at end of file
... ...
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/tool/method/package-info.java 0 → 100644
  1 +/**
  2 + * 参考 <a href="https://docs.spring.io/spring-ai/reference/api/tools.html#_methods_as_tools">Tool Calling —— Methods as Tools</a>
  3 + */
  4 +package cn.iocoder.yudao.module.ai.tool.method;
0 5 \ No newline at end of file
... ...
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/util/AiUtils.java
... ... @@ -8,18 +8,20 @@ import cn.iocoder.yudao.module.ai.enums.model.AiPlatformEnum;
8 8 import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatOptions;
9 9 import org.springaicommunity.moonshot.MoonshotChatOptions;
10 10 import org.springaicommunity.qianfan.QianFanChatOptions;
  11 +import org.springframework.ai.anthropic.AnthropicChatOptions;
11 12 import org.springframework.ai.azure.openai.AzureOpenAiChatOptions;
12 13 import org.springframework.ai.chat.messages.*;
  14 +import org.springframework.ai.chat.model.ChatResponse;
13 15 import org.springframework.ai.chat.prompt.ChatOptions;
  16 +import org.springframework.ai.deepseek.DeepSeekAssistantMessage;
  17 +import org.springframework.ai.deepseek.DeepSeekChatOptions;
14 18 import org.springframework.ai.minimax.MiniMaxChatOptions;
15 19 import org.springframework.ai.ollama.api.OllamaOptions;
16 20 import org.springframework.ai.openai.OpenAiChatOptions;
  21 +import org.springframework.ai.tool.ToolCallback;
17 22 import org.springframework.ai.zhipuai.ZhiPuAiChatOptions;
18 23  
19   -import java.util.Collections;
20   -import java.util.HashMap;
21   -import java.util.Map;
22   -import java.util.Set;
  24 +import java.util.*;
23 25  
24 26 /**
25 27 * Spring AI 工具类
... ... @@ -36,40 +38,47 @@ public class AiUtils {
36 38 }
37 39  
38 40 public static ChatOptions buildChatOptions(AiPlatformEnum platform, String model, Double temperature, Integer maxTokens,
39   - Set<String> toolNames, Map<String, Object> toolContext) {
40   - toolNames = ObjUtil.defaultIfNull(toolNames, Collections.emptySet());
  41 + List<ToolCallback> toolCallbacks, Map<String, Object> toolContext) {
  42 + toolCallbacks = ObjUtil.defaultIfNull(toolCallbacks, Collections.emptyList());
41 43 toolContext = ObjUtil.defaultIfNull(toolContext, Collections.emptyMap());
42 44 // noinspection EnhancedSwitchMigration
43 45 switch (platform) {
44 46 case TONG_YI:
45 47 return DashScopeChatOptions.builder().withModel(model).withTemperature(temperature).withMaxToken(maxTokens)
46   - .withToolNames(toolNames).withToolContext(toolContext).build();
  48 + .withEnableThinking(true) // TODO 芋艿:默认都开启 thinking 模式,后续可以让用户配置
  49 + .withToolCallbacks(toolCallbacks).withToolContext(toolContext).build();
47 50 case YI_YAN:
48 51 return QianFanChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens).build();
  52 + case DEEP_SEEK:
  53 + case DOU_BAO: // 复用 DeepSeek 客户端
  54 + case HUN_YUAN: // 复用 DeepSeek 客户端
  55 + case SILICON_FLOW: // 复用 DeepSeek 客户端
  56 + case XING_HUO: // 复用 DeepSeek 客户端
  57 + return DeepSeekChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens)
  58 + .toolCallbacks(toolCallbacks).toolContext(toolContext).build();
49 59 case ZHI_PU:
50 60 return ZhiPuAiChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens)
51   - .toolNames(toolNames).toolContext(toolContext).build();
  61 + .toolCallbacks(toolCallbacks).toolContext(toolContext).build();
52 62 case MINI_MAX:
53 63 return MiniMaxChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens)
54   - .toolNames(toolNames).toolContext(toolContext).build();
  64 + .toolCallbacks(toolCallbacks).toolContext(toolContext).build();
55 65 case MOONSHOT:
56 66 return MoonshotChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens)
57   - .toolNames(toolNames).toolContext(toolContext).build();
  67 + .toolCallbacks(toolCallbacks).toolContext(toolContext).build();
58 68 case OPENAI:
59   - case DEEP_SEEK: // 复用 OpenAI 客户端
60   - case DOU_BAO: // 复用 OpenAI 客户端
61   - case HUN_YUAN: // 复用 OpenAI 客户端
62   - case XING_HUO: // 复用 OpenAI 客户端
63   - case SILICON_FLOW: // 复用 OpenAI 客户端
  69 + case GEMINI: // 复用 OpenAI 客户端
64 70 case BAI_CHUAN: // 复用 OpenAI 客户端
65 71 return OpenAiChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens)
66   - .toolNames(toolNames).toolContext(toolContext).build();
  72 + .toolCallbacks(toolCallbacks).toolContext(toolContext).build();
67 73 case AZURE_OPENAI:
68 74 return AzureOpenAiChatOptions.builder().deploymentName(model).temperature(temperature).maxTokens(maxTokens)
69   - .toolNames(toolNames).toolContext(toolContext).build();
  75 + .toolCallbacks(toolCallbacks).toolContext(toolContext).build();
  76 + case ANTHROPIC:
  77 + return AnthropicChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens)
  78 + .toolCallbacks(toolCallbacks).toolContext(toolContext).build();
70 79 case OLLAMA:
71 80 return OllamaOptions.builder().model(model).temperature(temperature).numPredict(maxTokens)
72   - .toolNames(toolNames).toolContext(toolContext).build();
  81 + .toolCallbacks(toolCallbacks).toolContext(toolContext).build();
73 82 default:
74 83 throw new IllegalArgumentException(StrUtil.format("未知平台({})", platform));
75 84 }
... ... @@ -98,4 +107,27 @@ public class AiUtils {
98 107 return context;
99 108 }
100 109  
  110 + @SuppressWarnings("ConstantValue")
  111 + public static String getChatResponseContent(ChatResponse response) {
  112 + if (response == null
  113 + || response.getResult() == null
  114 + || response.getResult().getOutput() == null) {
  115 + return null;
  116 + }
  117 + return response.getResult().getOutput().getText();
  118 + }
  119 +
  120 + @SuppressWarnings("ConstantValue")
  121 + public static String getChatResponseReasoningContent(ChatResponse response) {
  122 + if (response == null
  123 + || response.getResult() == null
  124 + || response.getResult().getOutput() == null) {
  125 + return null;
  126 + }
  127 + if (response.getResult().getOutput() instanceof DeepSeekAssistantMessage) {
  128 + return ((DeepSeekAssistantMessage) (response.getResult().getOutput())).getReasoningContent();
  129 + }
  130 + return null;
  131 + }
  132 +
101 133 }
102 134 \ No newline at end of file
... ...
yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/AnthropicChatModelTest.java 0 → 100644
  1 +package cn.iocoder.yudao.module.ai.framework.ai.core.model.chat;
  2 +
  3 +import org.junit.jupiter.api.Disabled;
  4 +import org.junit.jupiter.api.Test;
  5 +import org.springframework.ai.anthropic.AnthropicChatModel;
  6 +import org.springframework.ai.anthropic.AnthropicChatOptions;
  7 +import org.springframework.ai.anthropic.api.AnthropicApi;
  8 +import org.springframework.ai.chat.messages.Message;
  9 +import org.springframework.ai.chat.messages.SystemMessage;
  10 +import org.springframework.ai.chat.messages.UserMessage;
  11 +import org.springframework.ai.chat.model.ChatResponse;
  12 +import org.springframework.ai.chat.prompt.Prompt;
  13 +import reactor.core.publisher.Flux;
  14 +
  15 +import java.util.ArrayList;
  16 +import java.util.List;
  17 +
  18 +/**
  19 + * {@link AnthropicChatModel} 集成测试类
  20 + *
  21 + * @author 芋道源码
  22 + */
  23 +public class AnthropicChatModelTest {
  24 +
  25 + private final AnthropicChatModel chatModel = AnthropicChatModel.builder()
  26 + .anthropicApi(AnthropicApi.builder()
  27 + .apiKey("sk-muubv7cXeLw0Etgs743f365cD5Ea44429946Fa7e672d8942")
  28 + .baseUrl("https://aihubmix.com")
  29 + .build())
  30 + .defaultOptions(AnthropicChatOptions.builder()
  31 + .model(AnthropicApi.ChatModel.CLAUDE_SONNET_4)
  32 + .temperature(0.7)
  33 + .maxTokens(4096)
  34 + .build())
  35 + .build();
  36 +
  37 + @Test
  38 + @Disabled
  39 + public void testCall() {
  40 + // 准备参数
  41 + List<Message> messages = new ArrayList<>();
  42 + messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。"));
  43 + messages.add(new UserMessage("1 + 1 = ?"));
  44 +
  45 + // 调用
  46 + ChatResponse response = chatModel.call(new Prompt(messages));
  47 + // 打印结果
  48 + System.out.println(response);
  49 + }
  50 +
  51 + @Test
  52 + @Disabled
  53 + public void testStream() {
  54 + // 准备参数
  55 + List<Message> messages = new ArrayList<>();
  56 + messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。"));
  57 + messages.add(new UserMessage("1 + 1 = ?"));
  58 +
  59 + // 调用
  60 + Flux<ChatResponse> flux = chatModel.stream(new Prompt(messages));
  61 + // 打印结果
  62 + flux.doOnNext(System.out::println).then().block();
  63 + }
  64 +
  65 + // TODO @芋艿:需要等 spring ai 升级:https://github.com/spring-projects/spring-ai/pull/2800
  66 + @Test
  67 + @Disabled
  68 + public void testStream_thinking() {
  69 + // 准备参数
  70 + List<Message> messages = new ArrayList<>();
  71 + messages.add(new UserMessage("thkinking 下,1+1 为什么等于 2 "));
  72 + AnthropicChatOptions options = AnthropicChatOptions.builder()
  73 + .model(AnthropicApi.ChatModel.CLAUDE_SONNET_4)
  74 + .thinking(AnthropicApi.ThinkingType.ENABLED, 3096)
  75 + .temperature(1D)
  76 + .build();
  77 +
  78 + // 调用
  79 + Flux<ChatResponse> flux = chatModel.stream(new Prompt(messages, options));
  80 + // 打印结果
  81 + flux.doOnNext(response -> {
  82 +// System.out.println(response);
  83 + System.out.println(response.getResult());
  84 + }).then().block();
  85 + }
  86 +
  87 +}
... ...
yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/DeepSeekChatModelTests.java
... ... @@ -60,4 +60,23 @@ public class DeepSeekChatModelTests {
60 60 flux.doOnNext(System.out::println).then().block();
61 61 }
62 62  
  63 + @Test
  64 + @Disabled
  65 + public void testStream_thinking() {
  66 + // 准备参数
  67 + List<Message> messages = new ArrayList<>();
  68 + messages.add(new UserMessage("详细分析下,如何设计一个电商系统?"));
  69 + DeepSeekChatOptions options = DeepSeekChatOptions.builder()
  70 + .model("deepseek-reasoner")
  71 + .build();
  72 +
  73 + // 调用
  74 + Flux<ChatResponse> flux = chatModel.stream(new Prompt(messages, options));
  75 + // 打印结果
  76 + flux.doOnNext(response -> {
  77 +// System.out.println(response);
  78 + System.out.println(response.getResult().getOutput());
  79 + }).then().block();
  80 + }
  81 +
63 82 }
... ...
yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/DouBaoChatModelTests.java
... ... @@ -8,9 +8,9 @@ import org.springframework.ai.chat.messages.SystemMessage;
8 8 import org.springframework.ai.chat.messages.UserMessage;
9 9 import org.springframework.ai.chat.model.ChatResponse;
10 10 import org.springframework.ai.chat.prompt.Prompt;
11   -import org.springframework.ai.openai.OpenAiChatModel;
12   -import org.springframework.ai.openai.OpenAiChatOptions;
13   -import org.springframework.ai.openai.api.OpenAiApi;
  11 +import org.springframework.ai.deepseek.DeepSeekChatModel;
  12 +import org.springframework.ai.deepseek.DeepSeekChatOptions;
  13 +import org.springframework.ai.deepseek.api.DeepSeekApi;
14 14 import reactor.core.publisher.Flux;
15 15  
16 16 import java.util.ArrayList;
... ... @@ -23,13 +23,18 @@ import java.util.List;
23 23 */
24 24 public class DouBaoChatModelTests {
25 25  
26   - private final OpenAiChatModel openAiChatModel = OpenAiChatModel.builder()
27   - .openAiApi(OpenAiApi.builder()
  26 + /**
  27 + * 相比 OpenAIChatModel 来说,DeepSeekChatModel 可以兼容豆包的 thinking 能力!
  28 + */
  29 + private final DeepSeekChatModel openAiChatModel = DeepSeekChatModel.builder()
  30 + .deepSeekApi(DeepSeekApi.builder()
28 31 .baseUrl(DouBaoChatModel.BASE_URL)
  32 + .completionsPath(DouBaoChatModel.COMPLETE_PATH)
29 33 .apiKey("5c1b5747-26d2-4ebd-a4e0-dd0e8d8b4272") // apiKey
30 34 .build())
31   - .defaultOptions(OpenAiChatOptions.builder()
  35 + .defaultOptions(DeepSeekChatOptions.builder()
32 36 .model("doubao-1-5-lite-32k-250115") // 模型(doubao)
  37 +// .model("doubao-seed-1-6-thinking-250715") // 模型(doubao)
33 38 // .model("deepseek-r1-250120") // 模型(deepseek)
34 39 .temperature(0.7)
35 40 .build())
... ... @@ -51,14 +56,13 @@ public class DouBaoChatModelTests {
51 56 System.out.println(response);
52 57 }
53 58  
54   - // TODO @芋艿:因为使用的是 v1 api,导致 deepseek-r1-250120 不返回 think 过程,后续需要优化
55 59 @Test
56 60 @Disabled
57 61 public void testStream() {
58 62 // 准备参数
59 63 List<Message> messages = new ArrayList<>();
60 64 messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。"));
61   - messages.add(new UserMessage("1 + 1 = ?"));
  65 + messages.add(new UserMessage("详细推理下,帮我设计一个用户中心!"));
62 66  
63 67 // 调用
64 68 Flux<ChatResponse> flux = chatModel.stream(new Prompt(messages));
... ... @@ -66,4 +70,23 @@ public class DouBaoChatModelTests {
66 70 flux.doOnNext(System.out::println).then().block();
67 71 }
68 72  
  73 + @Test
  74 + @Disabled
  75 + public void testStream_thinking() {
  76 + // 准备参数
  77 + List<Message> messages = new ArrayList<>();
  78 + messages.add(new UserMessage("详细分析下,如何设计一个电商系统?"));
  79 + DeepSeekChatOptions options = DeepSeekChatOptions.builder()
  80 + .model("doubao-seed-1-6-thinking-250715")
  81 + .build();
  82 +
  83 + // 调用
  84 + Flux<ChatResponse> flux = chatModel.stream(new Prompt(messages, options));
  85 + // 打印结果
  86 + flux.doOnNext(response -> {
  87 +// System.out.println(response);
  88 + System.out.println(response.getResult().getOutput());
  89 + }).then().block();
  90 + }
  91 +
69 92 }
... ...
yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/GeminiChatModelTests.java 0 → 100644
  1 +package cn.iocoder.yudao.module.ai.framework.ai.core.model.chat;
  2 +
  3 +import cn.iocoder.yudao.module.ai.framework.ai.core.model.gemini.GeminiChatModel;
  4 +import org.junit.jupiter.api.Disabled;
  5 +import org.junit.jupiter.api.Test;
  6 +import org.springframework.ai.chat.messages.Message;
  7 +import org.springframework.ai.chat.messages.SystemMessage;
  8 +import org.springframework.ai.chat.messages.UserMessage;
  9 +import org.springframework.ai.chat.model.ChatResponse;
  10 +import org.springframework.ai.chat.prompt.Prompt;
  11 +import org.springframework.ai.openai.OpenAiChatModel;
  12 +import org.springframework.ai.openai.OpenAiChatOptions;
  13 +import org.springframework.ai.openai.api.OpenAiApi;
  14 +import reactor.core.publisher.Flux;
  15 +
  16 +import java.util.ArrayList;
  17 +import java.util.List;
  18 +
  19 +/**
  20 + * {@link GeminiChatModel} 集成测试
  21 + *
  22 + * @author 芋道源码
  23 + */
  24 +public class GeminiChatModelTests {
  25 +
  26 + private final OpenAiChatModel openAiChatModel = OpenAiChatModel.builder()
  27 + .openAiApi(OpenAiApi.builder()
  28 + .baseUrl(GeminiChatModel.BASE_URL)
  29 + .completionsPath(GeminiChatModel.COMPLETE_PATH)
  30 + .apiKey("AIzaSyAVoBxgoFvvte820vEQMma2LKBnC98bqMQ")
  31 + .build())
  32 + .defaultOptions(OpenAiChatOptions.builder()
  33 + .model(GeminiChatModel.MODEL_DEFAULT) // 模型
  34 + .temperature(0.7)
  35 + .build())
  36 + .build();
  37 +
  38 + private final GeminiChatModel chatModel = new GeminiChatModel(openAiChatModel);
  39 +
  40 + @Test
  41 + @Disabled
  42 + public void testCall() {
  43 + // 准备参数
  44 + List<Message> messages = new ArrayList<>();
  45 + messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。"));
  46 + messages.add(new UserMessage("1 + 1 = ?"));
  47 +
  48 + // 调用
  49 + ChatResponse response = chatModel.call(new Prompt(messages));
  50 + // 打印结果
  51 + System.out.println(response);
  52 + }
  53 +
  54 + @Test
  55 + @Disabled
  56 + public void testStream() {
  57 + // 准备参数
  58 + List<Message> messages = new ArrayList<>();
  59 + messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。"));
  60 + messages.add(new UserMessage("1 + 1 = ?"));
  61 +
  62 + // 调用
  63 + Flux<ChatResponse> flux = chatModel.stream(new Prompt(messages));
  64 + // 打印结果
  65 + flux.doOnNext(System.out::println).then().block();
  66 + }
  67 +
  68 +}
... ...
yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/HunYuanChatModelTests.java
... ... @@ -8,9 +8,9 @@ import org.springframework.ai.chat.messages.SystemMessage;
8 8 import org.springframework.ai.chat.messages.UserMessage;
9 9 import org.springframework.ai.chat.model.ChatResponse;
10 10 import org.springframework.ai.chat.prompt.Prompt;
11   -import org.springframework.ai.openai.OpenAiChatModel;
12   -import org.springframework.ai.openai.OpenAiChatOptions;
13   -import org.springframework.ai.openai.api.OpenAiApi;
  11 +import org.springframework.ai.deepseek.DeepSeekChatModel;
  12 +import org.springframework.ai.deepseek.DeepSeekChatOptions;
  13 +import org.springframework.ai.deepseek.api.DeepSeekApi;
14 14 import reactor.core.publisher.Flux;
15 15  
16 16 import java.util.ArrayList;
... ... @@ -23,12 +23,13 @@ import java.util.List;
23 23 */
24 24 public class HunYuanChatModelTests {
25 25  
26   - private final OpenAiChatModel openAiChatModel = OpenAiChatModel.builder()
27   - .openAiApi(OpenAiApi.builder()
  26 + private final DeepSeekChatModel openAiChatModel = DeepSeekChatModel.builder()
  27 + .deepSeekApi(DeepSeekApi.builder()
28 28 .baseUrl(HunYuanChatModel.BASE_URL)
29   - .apiKey("sk-bcd") // apiKey
  29 + .completionsPath(HunYuanChatModel.COMPLETE_PATH)
  30 + .apiKey("sk-abc") // apiKey
30 31 .build())
31   - .defaultOptions(OpenAiChatOptions.builder()
  32 + .defaultOptions(DeepSeekChatOptions.builder()
32 33 .model(HunYuanChatModel.MODEL_DEFAULT) // 模型
33 34 .temperature(0.7)
34 35 .build())
... ... @@ -64,12 +65,33 @@ public class HunYuanChatModelTests {
64 65 flux.doOnNext(System.out::println).then().block();
65 66 }
66 67  
67   - private final OpenAiChatModel deepSeekOpenAiChatModel = OpenAiChatModel.builder()
68   - .openAiApi(OpenAiApi.builder()
  68 + @Test
  69 + @Disabled
  70 + public void testStream_thinking() {
  71 + // 准备参数
  72 + List<Message> messages = new ArrayList<>();
  73 + messages.add(new UserMessage("详细分析下,如何设计一个电商系统?"));
  74 + DeepSeekChatOptions options = DeepSeekChatOptions.builder()
  75 + .model("hunyuan-a13b")
  76 +// .model("hunyuan-turbos-latest")
  77 + .build();
  78 +
  79 + // 调用
  80 + Flux<ChatResponse> flux = chatModel.stream(new Prompt(messages, options));
  81 + // 打印结果
  82 + flux.doOnNext(response -> {
  83 +// System.out.println(response);
  84 + System.out.println(response.getResult().getOutput());
  85 + }).then().block();
  86 + }
  87 +
  88 + private final DeepSeekChatModel deepSeekOpenAiChatModel = DeepSeekChatModel.builder()
  89 + .deepSeekApi(DeepSeekApi.builder()
69 90 .baseUrl(HunYuanChatModel.DEEP_SEEK_BASE_URL)
  91 + .completionsPath(HunYuanChatModel.COMPLETE_PATH)
70 92 .apiKey("sk-abc") // apiKey
71 93 .build())
72   - .defaultOptions(OpenAiChatOptions.builder()
  94 + .defaultOptions(DeepSeekChatOptions.builder()
73 95 // .model(HunYuanChatModel.DEEP_SEEK_MODEL_DEFAULT) // 模型("deepseek-v3")
74 96 .model("deepseek-r1") // 模型("deepseek-r1")
75 97 .temperature(0.7)
... ... @@ -94,7 +116,7 @@ public class HunYuanChatModelTests {
94 116  
95 117 @Test
96 118 @Disabled
97   - public void testStream_deekseek() {
  119 + public void testStream_deepseek() {
98 120 // 准备参数
99 121 List<Message> messages = new ArrayList<>();
100 122 messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。"));
... ... @@ -106,5 +128,23 @@ public class HunYuanChatModelTests {
106 128 flux.doOnNext(System.out::println).then().block();
107 129 }
108 130  
  131 + @Test
  132 + @Disabled
  133 + public void testStream_deepseek_thinking() {
  134 + // 准备参数
  135 + List<Message> messages = new ArrayList<>();
  136 + messages.add(new UserMessage("详细分析下,如何设计一个电商系统?"));
  137 + DeepSeekChatOptions options = DeepSeekChatOptions.builder()
  138 + .model("deepseek-r1")
  139 + .build();
  140 +
  141 + // 调用
  142 + Flux<ChatResponse> flux = deepSeekChatModel.stream(new Prompt(messages, options));
  143 + // 打印结果
  144 + flux.doOnNext(response -> {
  145 +// System.out.println(response);
  146 + System.out.println(response.getResult().getOutput());
  147 + }).then().block();
  148 + }
109 149  
110 150 }
... ...
yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/LlamaChatModelTests.java
1 1 package cn.iocoder.yudao.module.ai.framework.ai.core.model.chat;
2 2  
  3 +import org.junit.jupiter.api.Disabled;
  4 +import org.junit.jupiter.api.Test;
  5 +import org.springframework.ai.chat.messages.Message;
  6 +import org.springframework.ai.chat.messages.SystemMessage;
  7 +import org.springframework.ai.chat.messages.UserMessage;
  8 +import org.springframework.ai.chat.model.ChatResponse;
  9 +import org.springframework.ai.chat.prompt.Prompt;
3 10 import org.springframework.ai.ollama.OllamaChatModel;
  11 +import org.springframework.ai.ollama.api.OllamaApi;
  12 +import org.springframework.ai.ollama.api.OllamaModel;
  13 +import org.springframework.ai.ollama.api.OllamaOptions;
  14 +import reactor.core.publisher.Flux;
  15 +
  16 +import java.util.ArrayList;
  17 +import java.util.List;
4 18  
5 19 /**
6 20 * {@link OllamaChatModel} 集成测试
... ... @@ -9,43 +23,65 @@ import org.springframework.ai.ollama.OllamaChatModel;
9 23 */
10 24 public class LlamaChatModelTests {
11 25  
12   -// private final OllamaChatModel chatModel = OllamaChatModel.builder()
13   -// .ollamaApi(new OllamaApi("http://127.0.0.1:11434")) // Ollama 服务地址
14   -// .defaultOptions(OllamaOptions.builder()
15   -// .model(OllamaModel.LLAMA3.getName()) // 模型
16   -// .build())
17   -// .build();
18   -//
19   -// @Test
20   -// @Disabled
21   -// public void testCall() {
22   -// // 准备参数
23   -// List<Message> messages = new ArrayList<>();
24   -// messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。"));
25   -// messages.add(new UserMessage("1 + 1 = ?"));
26   -//
27   -// // 调用
28   -// ChatResponse response = chatModel.call(new Prompt(messages));
29   -// // 打印结果
30   -// System.out.println(response);
31   -// System.out.println(response.getResult().getOutput());
32   -// }
33   -//
34   -// @Test
35   -// @Disabled
36   -// public void testStream() {
37   -// // 准备参数
38   -// List<Message> messages = new ArrayList<>();
39   -// messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。"));
40   -// messages.add(new UserMessage("1 + 1 = ?"));
41   -//
42   -// // 调用
43   -// Flux<ChatResponse> flux = chatModel.stream(new Prompt(messages));
44   -// // 打印结果
45   -// flux.doOnNext(response -> {
46   -//// System.out.println(response);
47   -// System.out.println(response.getResult().getOutput());
48   -// }).then().block();
49   -// }
  26 + private final OllamaChatModel chatModel = OllamaChatModel.builder()
  27 + .ollamaApi(OllamaApi.builder()
  28 + .baseUrl("http://127.0.0.1:11434") // Ollama 服务地址
  29 + .build())
  30 + .defaultOptions(OllamaOptions.builder()
  31 + .model(OllamaModel.LLAMA3.getName()) // 模型
  32 + .build())
  33 + .build();
  34 +
  35 + @Test
  36 + @Disabled
  37 + public void testCall() {
  38 + // 准备参数
  39 + List<Message> messages = new ArrayList<>();
  40 + messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。"));
  41 + messages.add(new UserMessage("1 + 1 = ?"));
  42 +
  43 + // 调用
  44 + ChatResponse response = chatModel.call(new Prompt(messages));
  45 + // 打印结果
  46 + System.out.println(response);
  47 + System.out.println(response.getResult().getOutput());
  48 + }
  49 +
  50 + @Test
  51 + @Disabled
  52 + public void testStream() {
  53 + // 准备参数
  54 + List<Message> messages = new ArrayList<>();
  55 + messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。"));
  56 + messages.add(new UserMessage("1 + 1 = ?"));
  57 +
  58 + // 调用
  59 + Flux<ChatResponse> flux = chatModel.stream(new Prompt(messages));
  60 + // 打印结果
  61 + flux.doOnNext(response -> {
  62 +// System.out.println(response);
  63 + System.out.println(response.getResult().getOutput());
  64 + }).then().block();
  65 + }
  66 +
  67 + @Test
  68 + @Disabled
  69 + public void testStream_thinking() {
  70 + // 准备参数
  71 + List<Message> messages = new ArrayList<>();
  72 + messages.add(new UserMessage("详细分析下,如何设计一个电商系统?"));
  73 + OllamaOptions options = OllamaOptions.builder()
  74 + .model("qwen3")
  75 + .build();
  76 +
  77 + // 调用
  78 + Flux<ChatResponse> flux = chatModel.stream(new Prompt(messages, options));
  79 + // 打印结果
  80 + flux.doOnNext(response -> {
  81 +// System.out.println(response);
  82 + System.out.println(response.getResult().getOutput());
  83 + }).then().block();
  84 + }
  85 +
50 86  
51 87 }
... ...
yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/MiniMaxChatModelTests.java
... ... @@ -59,4 +59,24 @@ public class MiniMaxChatModelTests {
59 59 }).then().block();
60 60 }
61 61  
  62 + // TODO @芋艿:暂时没解析 reasoning_content 结果,需要等官方修复
  63 + @Test
  64 + @Disabled
  65 + public void testStream_thinking() {
  66 + // 准备参数
  67 + List<Message> messages = new ArrayList<>();
  68 + messages.add(new UserMessage("详细分析下,如何设计一个电商系统?"));
  69 + MiniMaxChatOptions options = MiniMaxChatOptions.builder()
  70 + .model("MiniMax-M1")
  71 + .build();
  72 +
  73 + // 调用
  74 + Flux<ChatResponse> flux = chatModel.stream(new Prompt(messages, options));
  75 + // 打印结果
  76 + flux.doOnNext(response -> {
  77 +// System.out.println(response);
  78 + System.out.println(response.getResult().getOutput());
  79 + }).then().block();
  80 + }
  81 +
62 82 }
... ...
yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/MoonshotChatModelTests.java
... ... @@ -63,4 +63,25 @@ public class MoonshotChatModelTests {
63 63 }).then().block();
64 64 }
65 65  
  66 + // TODO @芋艿:暂时没解析 reasoning_content 结果,需要等官方修复
  67 + @Test
  68 + @Disabled
  69 + public void testStream_thinking() {
  70 + // 准备参数
  71 + List<Message> messages = new ArrayList<>();
  72 + messages.add(new UserMessage("详细分析下,如何设计一个电商系统?"));
  73 + MoonshotChatOptions options = MoonshotChatOptions.builder()
  74 +// .model("kimi-k2-0711-preview")
  75 + .model("kimi-thinking-preview")
  76 + .build();
  77 +
  78 + // 调用
  79 + Flux<ChatResponse> flux = chatModel.stream(new Prompt(messages, options));
  80 + // 打印结果
  81 + flux.doOnNext(response -> {
  82 +// System.out.println(response);
  83 + System.out.println(response.getResult().getOutput());
  84 + }).then().block();
  85 + }
  86 +
66 87 }
... ...
yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/OpenAIChatModelTests.java
1 1 package cn.iocoder.yudao.module.ai.framework.ai.core.model.chat;
2 2  
  3 +import com.azure.ai.openai.models.ReasoningEffortValue;
3 4 import org.junit.jupiter.api.Disabled;
4 5 import org.junit.jupiter.api.Test;
5 6 import org.springframework.ai.chat.messages.Message;
... ... @@ -25,10 +26,11 @@ public class OpenAIChatModelTests {
25 26 private final OpenAiChatModel chatModel = OpenAiChatModel.builder()
26 27 .openAiApi(OpenAiApi.builder()
27 28 .baseUrl("https://api.holdai.top")
28   - .apiKey("sk-PytRecQlmjEteoa2RRN6cGnwslo72UUPLQVNEMS6K9yjbmpD") // apiKey
  29 + .apiKey("sk-z5joyRoV1iFEnh2SAi8QPNrIZTXyQSyxTmD5CoNDQbFixK2l") // apiKey
29 30 .build())
30 31 .defaultOptions(OpenAiChatOptions.builder()
31   - .model(OpenAiApi.ChatModel.GPT_4_1_NANO) // 模型
  32 + .model("gpt-5-nano-2025-08-07") // 模型
  33 +// .model(OpenAiApi.ChatModel.O1) // 模型
32 34 .temperature(0.7)
33 35 .build())
34 36 .build();
... ... @@ -54,7 +56,7 @@ public class OpenAIChatModelTests {
54 56 // 准备参数
55 57 List<Message> messages = new ArrayList<>();
56 58 messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。"));
57   - messages.add(new UserMessage("1 + 1 = ?"));
  59 + messages.add(new UserMessage("帮我推理下,怎么实现一个用户中心!"));
58 60  
59 61 // 调用
60 62 Flux<ChatResponse> flux = chatModel.stream(new Prompt(messages));
... ... @@ -65,4 +67,29 @@ public class OpenAIChatModelTests {
65 67 }).then().block();
66 68 }
67 69  
  70 + // TODO @芋艿:无法触发思考的字段返回,需要 response api:https://github.com/spring-projects/spring-ai/issues/2962
  71 + @Test
  72 + @Disabled
  73 + public void testStream_thinking() {
  74 + // 准备参数
  75 + List<Message> messages = new ArrayList<>();
  76 + messages.add(new UserMessage("详细分析下,如何设计一个电商系统?"));
  77 + OpenAiChatOptions options = OpenAiChatOptions.builder()
  78 + .model("gpt-5")
  79 +// .model(OpenAiApi.ChatModel.O4_MINI)
  80 +// .model("o3-pro")
  81 + .reasoningEffort(ReasoningEffortValue.LOW.getValue())
  82 + .build();
  83 +
  84 + // 调用
  85 + Flux<ChatResponse> flux = chatModel.stream(new Prompt(messages, options));
  86 + // 打印结果
  87 + flux.doOnNext(response -> {
  88 +// System.out.println(response);
  89 + System.out.println(response.getResult().getOutput());
  90 + }).then().block();
  91 + }
  92 +
  93 +
  94 +
68 95 }
... ...
yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/SiliconFlowChatModelTests.java
... ... @@ -9,9 +9,9 @@ import org.springframework.ai.chat.messages.SystemMessage;
9 9 import org.springframework.ai.chat.messages.UserMessage;
10 10 import org.springframework.ai.chat.model.ChatResponse;
11 11 import org.springframework.ai.chat.prompt.Prompt;
12   -import org.springframework.ai.openai.OpenAiChatModel;
13   -import org.springframework.ai.openai.OpenAiChatOptions;
14   -import org.springframework.ai.openai.api.OpenAiApi;
  12 +import org.springframework.ai.deepseek.DeepSeekChatModel;
  13 +import org.springframework.ai.deepseek.DeepSeekChatOptions;
  14 +import org.springframework.ai.deepseek.api.DeepSeekApi;
15 15 import reactor.core.publisher.Flux;
16 16  
17 17 import java.util.ArrayList;
... ... @@ -24,12 +24,12 @@ import java.util.List;
24 24 */
25 25 public class SiliconFlowChatModelTests {
26 26  
27   - private final OpenAiChatModel openAiChatModel = OpenAiChatModel.builder()
28   - .openAiApi(OpenAiApi.builder()
  27 + private final DeepSeekChatModel openAiChatModel = DeepSeekChatModel.builder()
  28 + .deepSeekApi(DeepSeekApi.builder()
29 29 .baseUrl(SiliconFlowApiConstants.DEFAULT_BASE_URL)
30 30 .apiKey("sk-epsakfenqnyzoxhmbucsxlhkdqlcbnimslqoivkshalvdozz") // apiKey
31 31 .build())
32   - .defaultOptions(OpenAiChatOptions.builder()
  32 + .defaultOptions(DeepSeekChatOptions.builder()
33 33 .model(SiliconFlowApiConstants.MODEL_DEFAULT) // 模型
34 34 // .model("deepseek-ai/DeepSeek-R1") // 模型(deepseek-ai/DeepSeek-R1)可用赠费
35 35 // .model("Pro/deepseek-ai/DeepSeek-R1") // 模型(Pro/deepseek-ai/DeepSeek-R1)需要付费
... ... @@ -67,4 +67,23 @@ public class SiliconFlowChatModelTests {
67 67 flux.doOnNext(System.out::println).then().block();
68 68 }
69 69  
  70 + @Test
  71 + @Disabled
  72 + public void testStream_thinking() {
  73 + // 准备参数
  74 + List<Message> messages = new ArrayList<>();
  75 + messages.add(new UserMessage("详细分析下,如何设计一个电商系统?"));
  76 + DeepSeekChatOptions options = DeepSeekChatOptions.builder()
  77 + .model("deepseek-ai/DeepSeek-R1")
  78 + .build();
  79 +
  80 + // 调用
  81 + Flux<ChatResponse> flux = chatModel.stream(new Prompt(messages, options));
  82 + // 打印结果
  83 + flux.doOnNext(response -> {
  84 +// System.out.println(response);
  85 + System.out.println(response.getResult().getOutput());
  86 + }).then().block();
  87 + }
  88 +
70 89 }
... ...
yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/TongYiChatModelTests.java
1 1 package cn.iocoder.yudao.module.ai.framework.ai.core.model.chat;
2 2  
  3 +import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
3 4 import com.alibaba.cloud.ai.dashscope.api.DashScopeApi;
4 5 import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatModel;
5 6 import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatOptions;
  7 +import com.alibaba.cloud.ai.dashscope.rerank.DashScopeRerankModel;
  8 +import com.alibaba.cloud.ai.dashscope.rerank.DashScopeRerankOptions;
  9 +import com.alibaba.cloud.ai.model.RerankModel;
  10 +import com.alibaba.cloud.ai.model.RerankOptions;
  11 +import com.alibaba.cloud.ai.model.RerankRequest;
  12 +import com.alibaba.cloud.ai.model.RerankResponse;
6 13 import org.junit.jupiter.api.Disabled;
7 14 import org.junit.jupiter.api.Test;
8 15 import org.springframework.ai.chat.messages.Message;
... ... @@ -10,11 +17,14 @@ import org.springframework.ai.chat.messages.SystemMessage;
10 17 import org.springframework.ai.chat.messages.UserMessage;
11 18 import org.springframework.ai.chat.model.ChatResponse;
12 19 import org.springframework.ai.chat.prompt.Prompt;
  20 +import org.springframework.ai.document.Document;
13 21 import reactor.core.publisher.Flux;
14 22  
15 23 import java.util.ArrayList;
16 24 import java.util.List;
17 25  
  26 +import static java.util.Arrays.asList;
  27 +
18 28 /**
19 29 * {@link DashScopeChatModel} 集成测试类
20 30 *
... ... @@ -26,11 +36,13 @@ public class TongYiChatModelTests {
26 36 .dashScopeApi(DashScopeApi.builder()
27 37 .apiKey("sk-47aa124781be4bfb95244cc62f63f7d0")
28 38 .build())
29   - .defaultOptions( DashScopeChatOptions.builder()
30   - .withModel("qwen1.5-72b-chat") // 模型
  39 + .defaultOptions(DashScopeChatOptions.builder()
  40 +// .withModel("qwen1.5-72b-chat") // 模型
  41 + .withModel("qwen3-235b-a22b-thinking-2507") // 模型
31 42 // .withModel("deepseek-r1") // 模型(deepseek-r1)
32 43 // .withModel("deepseek-v3") // 模型(deepseek-v3)
33 44 // .withModel("deepseek-r1-distill-qwen-1.5b") // 模型(deepseek-r1-distill-qwen-1.5b)
  45 +// .withEnableThinking(true)
34 46 .build())
35 47 .build();
36 48  
... ... @@ -54,8 +66,8 @@ public class TongYiChatModelTests {
54 66 public void testStream() {
55 67 // 准备参数
56 68 List<Message> messages = new ArrayList<>();
57   - messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。"));
58   - messages.add(new UserMessage("1 + 1 = ?"));
  69 +// messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。"));
  70 + messages.add(new UserMessage("帮我推理下,怎么实现一个用户中心!"));
59 71  
60 72 // 调用
61 73 Flux<ChatResponse> flux = chatModel.stream(new Prompt(messages));
... ... @@ -66,4 +78,52 @@ public class TongYiChatModelTests {
66 78 }).then().block();
67 79 }
68 80  
  81 + @Test
  82 + @Disabled
  83 + public void testStream_thinking() {
  84 + // 准备参数
  85 + List<Message> messages = new ArrayList<>();
  86 + messages.add(new UserMessage("详细分析下,如何设计一个电商系统?"));
  87 + DashScopeChatOptions options = DashScopeChatOptions.builder()
  88 + .withModel("qwen3-235b-a22b-thinking-2507")
  89 +// .withModel("qwen-max-2025-01-25")
  90 + .withEnableThinking(true) // 必须设置,否则会报错
  91 + .build();
  92 +
  93 + // 调用
  94 + Flux<ChatResponse> flux = chatModel.stream(new Prompt(messages, options));
  95 + // 打印结果
  96 + flux.doOnNext(response -> {
  97 +// System.out.println(response);
  98 + System.out.println(response.getResult().getOutput());
  99 + }).then().block();
  100 + }
  101 +
  102 + @Test
  103 + @Disabled
  104 + public void testRerank() {
  105 + // 准备环境
  106 + RerankModel rerankModel = new DashScopeRerankModel(
  107 + DashScopeApi.builder()
  108 + .apiKey("sk-47aa124781be4bfb95244cc62f63f7d0")
  109 + .build());
  110 + // 准备参数
  111 + String query = "spring";
  112 + Document document01 = new Document("abc");
  113 + Document document02 = new Document("sapring");
  114 + RerankOptions options = DashScopeRerankOptions.builder()
  115 + .withTopN(1)
  116 + .withModel("gte-rerank-v2")
  117 + .build();
  118 + RerankRequest rerankRequest = new RerankRequest(
  119 + query,
  120 + asList(document01, document02),
  121 + options);
  122 +
  123 + // 调用
  124 + RerankResponse call = rerankModel.call(rerankRequest);
  125 + // 打印结果
  126 + System.out.println(JsonUtils.toJsonPrettyString(call));
  127 + }
  128 +
69 129 }
... ...
yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/XingHuoChatModelTests.java
... ... @@ -8,9 +8,9 @@ import org.springframework.ai.chat.messages.SystemMessage;
8 8 import org.springframework.ai.chat.messages.UserMessage;
9 9 import org.springframework.ai.chat.model.ChatResponse;
10 10 import org.springframework.ai.chat.prompt.Prompt;
11   -import org.springframework.ai.openai.OpenAiChatModel;
12   -import org.springframework.ai.openai.OpenAiChatOptions;
13   -import org.springframework.ai.openai.api.OpenAiApi;
  11 +import org.springframework.ai.deepseek.DeepSeekChatModel;
  12 +import org.springframework.ai.deepseek.DeepSeekChatOptions;
  13 +import org.springframework.ai.deepseek.api.DeepSeekApi;
14 14 import reactor.core.publisher.Flux;
15 15  
16 16 import java.util.ArrayList;
... ... @@ -23,13 +23,15 @@ import java.util.List;
23 23 */
24 24 public class XingHuoChatModelTests {
25 25  
26   - private final OpenAiChatModel openAiChatModel = OpenAiChatModel.builder()
27   - .openAiApi(OpenAiApi.builder()
28   - .baseUrl(XingHuoChatModel.BASE_URL)
  26 + private final DeepSeekChatModel openAiChatModel = DeepSeekChatModel.builder()
  27 + .deepSeekApi(DeepSeekApi.builder()
  28 + .baseUrl(XingHuoChatModel.BASE_URL_V2)
  29 + .completionsPath(XingHuoChatModel.BASE_COMPLETIONS_PATH_V2)
29 30 .apiKey("75b161ed2aef4719b275d6e7f2a4d4cd:YWYxYWI2MTA4ODI2NGZlYTQyNjAzZTcz") // appKey:secretKey
30 31 .build())
31   - .defaultOptions(OpenAiChatOptions.builder()
32   - .model("generalv3.5") // 模型
  32 + .defaultOptions(DeepSeekChatOptions.builder()
  33 +// .model("generalv3.5") // 模型
  34 + .model("x1") // 模型
33 35 .temperature(0.7)
34 36 .build())
35 37 .build();
... ... @@ -64,4 +66,23 @@ public class XingHuoChatModelTests {
64 66 flux.doOnNext(System.out::println).then().block();
65 67 }
66 68  
  69 + @Test
  70 + @Disabled
  71 + public void testStream_thinking() {
  72 + // 准备参数
  73 + List<Message> messages = new ArrayList<>();
  74 + messages.add(new UserMessage("详细分析下,如何设计一个电商系统?"));
  75 + DeepSeekChatOptions options = DeepSeekChatOptions.builder()
  76 + .model("x1")
  77 + .build();
  78 +
  79 + // 调用
  80 + Flux<ChatResponse> flux = chatModel.stream(new Prompt(messages, options));
  81 + // 打印结果
  82 + flux.doOnNext(response -> {
  83 +// System.out.println(response);
  84 + System.out.println(response.getResult().getOutput());
  85 + }).then().block();
  86 + }
  87 +
67 88 }
... ...
yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/ZhiPuAiChatModelTests.java
... ... @@ -23,7 +23,7 @@ import java.util.List;
23 23 public class ZhiPuAiChatModelTests {
24 24  
25 25 private final ZhiPuAiChatModel chatModel = new ZhiPuAiChatModel(
26   - new ZhiPuAiApi("32f84543e54eee31f8d56b2bd6020573.3vh9idLJZ2ZhxDEs"), // 密钥
  26 + new ZhiPuAiApi("2f35fb6ca4ea41fab898729b7fac086c.6ESSfPcCkxaKEUlR"), // 密钥
27 27 ZhiPuAiChatOptions.builder()
28 28 .model(ZhiPuAiApi.ChatModel.GLM_4.getName()) // 模型
29 29 .build()
... ... @@ -61,4 +61,24 @@ public class ZhiPuAiChatModelTests {
61 61 }).then().block();
62 62 }
63 63  
  64 + // TODO @芋艿:暂时没解析 reasoning_content 结果,需要等官方修复
  65 + @Test
  66 + @Disabled
  67 + public void testStream_thinking() {
  68 + // 准备参数
  69 + List<Message> messages = new ArrayList<>();
  70 + messages.add(new UserMessage("详细分析下,如何设计一个电商系统?"));
  71 + ZhiPuAiChatOptions options = ZhiPuAiChatOptions.builder()
  72 + .model("GLM-4.5")
  73 + .build();
  74 +
  75 + // 调用
  76 + Flux<ChatResponse> flux = chatModel.stream(new Prompt(messages, options));
  77 + // 打印结果
  78 + flux.doOnNext(response -> {
  79 +// System.out.println(response);
  80 + System.out.println(response.getResult().getOutput());
  81 + }).then().block();
  82 + }
  83 +
64 84 }
... ...
yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/image/TongYiImagesModelTest.java
... ... @@ -16,8 +16,11 @@ import org.springframework.ai.image.ImageResponse;
16 16 */
17 17 public class TongYiImagesModelTest {
18 18  
19   - private final DashScopeImageModel imageModel = new DashScopeImageModel(
20   - new DashScopeImageApi("sk-7d903764249848cfa912733146da12d1"));
  19 + private final DashScopeImageModel imageModel = DashScopeImageModel.builder()
  20 + .dashScopeApi(DashScopeImageApi.builder()
  21 + .apiKey("sk-47aa124781be4bfb95244cc62f63f7d0")
  22 + .build())
  23 + .build();
21 24  
22 25 @Test
23 26 @Disabled
... ...
yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/mcp/DouBaoMcpTests.java
... ... @@ -36,44 +36,47 @@ public class DouBaoMcpTests {
36 36  
37 37 @Test
38 38 public void testMcpGetUserInfo() {
39   -
40 39 // 打印结果
41 40 System.out.println(chatClient.prompt()
42 41 .user("目前有哪些工具可以使用")
43 42 .call()
44 43 .content());
45 44 System.out.println("====================================");
  45 +
46 46 // 打印结果
47 47 System.out.println(chatClient.prompt()
48 48 .user("小新的年龄是多少")
49 49 .call()
50 50 .content());
51 51 System.out.println("====================================");
  52 +
52 53 // 打印结果
53 54 System.out.println(chatClient.prompt()
54 55 .user("获取小新的基本信息")
55 56 .call()
56 57 .content());
57 58 System.out.println("====================================");
  59 +
58 60 // 打印结果
59 61 System.out.println(chatClient.prompt()
60 62 .user("小新是什么职业的")
61 63 .call()
62 64 .content());
63 65 System.out.println("====================================");
  66 +
64 67 // 打印结果
65 68 System.out.println(chatClient.prompt()
66 69 .user("小新的教育背景")
67 70 .call()
68 71 .content());
69 72 System.out.println("====================================");
  73 +
70 74 // 打印结果
71 75 System.out.println(chatClient.prompt()
72 76 .user("小新的兴趣爱好是什么")
73 77 .call()
74 78 .content());
75 79 System.out.println("====================================");
76   -
77 80 }
78 81  
79 82  
... ...
yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/websearch/AiBoChaWebSearchClientTest.java 0 → 100644
  1 +package cn.iocoder.yudao.module.ai.framework.ai.core.websearch;
  2 +
  3 +import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
  4 +import cn.iocoder.yudao.module.ai.framework.ai.core.webserch.AiWebSearchRequest;
  5 +import cn.iocoder.yudao.module.ai.framework.ai.core.webserch.AiWebSearchResponse;
  6 +import cn.iocoder.yudao.module.ai.framework.ai.core.webserch.bocha.AiBoChaWebSearchClient;
  7 +import org.junit.jupiter.api.Test;
  8 +
  9 +/**
  10 + * {@link AiBoChaWebSearchClient} 集成测试类
  11 + *
  12 + * @author 芋道源码
  13 + */
  14 +public class AiBoChaWebSearchClientTest {
  15 +
  16 + private final AiBoChaWebSearchClient webSearchClient = new AiBoChaWebSearchClient(
  17 + "sk-40500e52840f4d24b956d0b1d80d9abe");
  18 +
  19 + @Test
  20 + public void testSearch() {
  21 + AiWebSearchRequest request = new AiWebSearchRequest()
  22 + .setQuery("阿里巴巴")
  23 + .setCount(3);
  24 + AiWebSearchResponse response = webSearchClient.search(request);
  25 + System.out.println(JsonUtils.toJsonPrettyString(response));
  26 + }
  27 +
  28 +}
0 29 \ No newline at end of file
... ...
yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/framework/file/core/utils/FileTypeUtils.java
... ... @@ -80,17 +80,17 @@ public class FileTypeUtils {
80 80 */
81 81 public static void writeAttachment(HttpServletResponse response, String filename, byte[] content) throws IOException {
82 82 // 设置 header 和 contentType
83   - String contentType = getMineType(content, filename);
84   - response.setContentType(contentType);
  83 + String mineType = getMineType(content, filename);
  84 + response.setContentType(mineType);
85 85 // 设置内容显示、下载文件名:https://www.cnblogs.com/wq-9/articles/12165056.html
86   - if (StrUtil.containsIgnoreCase(contentType, "image/")) {
  86 + if (isImage(mineType)) {
87 87 // 参见 https://github.com/YunaiV/ruoyi-vue-pro/issues/692 讨论
88 88 response.setHeader("Content-Disposition", "inline;filename=" + HttpUtils.encodeUtf8(filename));
89 89 } else {
90 90 response.setHeader("Content-Disposition", "attachment;filename=" + HttpUtils.encodeUtf8(filename));
91 91 }
92 92 // 针对 video 的特殊处理,解决视频地址在移动端播放的兼容性问题
93   - if (StrUtil.containsIgnoreCase(contentType, "video")) {
  93 + if (StrUtil.containsIgnoreCase(mineType, "video")) {
94 94 response.setHeader("Content-Length", String.valueOf(content.length));
95 95 response.setHeader("Content-Range", "bytes 0-" + (content.length - 1) + "/" + content.length);
96 96 response.setHeader("Accept-Ranges", "bytes");
... ... @@ -99,4 +99,14 @@ public class FileTypeUtils {
99 99 IoUtil.write(response.getOutputStream(), false, content);
100 100 }
101 101  
  102 + /**
  103 + * 判断是否是图片
  104 + *
  105 + * @param mineType 类型
  106 + * @return 是否是图片
  107 + */
  108 + public static boolean isImage(String mineType) {
  109 + return StrUtil.startWith(mineType, "image/");
  110 + }
  111 +
102 112 }
... ...
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/framework/justauth/core/AuthRequestFactory.java
... ... @@ -77,8 +77,7 @@ public class AuthRequestFactory {
77 77 extendList = extend.getConfig()
78 78 .keySet()
79 79 .stream()
80   - .map(String::toUpperCase)
81   - .filter(names::contains)
  80 + .filter(x -> names.contains(x.toUpperCase()))
82 81 .collect(Collectors.toList());
83 82 }
84 83  
... ...
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/AliyunSmsClient.java
... ... @@ -113,13 +113,14 @@ public class AliyunSmsClient extends AbstractSmsClient {
113 113 }
114 114  
115 115 @VisibleForTesting
  116 + @SuppressWarnings("EnhancedSwitchMigration")
116 117 Integer convertSmsTemplateAuditStatus(Integer templateStatus) {
117   - return switch (templateStatus) {
118   - case 0 -> SmsTemplateAuditStatusEnum.CHECKING.getStatus();
119   - case 1 -> SmsTemplateAuditStatusEnum.SUCCESS.getStatus();
120   - case 2 -> SmsTemplateAuditStatusEnum.FAIL.getStatus();
121   - default -> throw new IllegalArgumentException(String.format("未知审核状态(%d)", templateStatus));
122   - };
  118 + switch (templateStatus) {
  119 + case 0: return SmsTemplateAuditStatusEnum.CHECKING.getStatus();
  120 + case 1: return SmsTemplateAuditStatusEnum.SUCCESS.getStatus();
  121 + case 2: return SmsTemplateAuditStatusEnum.FAIL.getStatus();
  122 + default: throw new IllegalArgumentException(String.format("未知审核状态(%d)", templateStatus));
  123 + }
123 124 }
124 125  
125 126 /**
... ... @@ -189,7 +190,7 @@ public class AliyunSmsClient extends AbstractSmsClient {
189 190 @SneakyThrows
190 191 private static String percentCode(String str) {
191 192 Assert.notNull(str, "str 不能为空");
192   - return URLEncoder.encode(str, StandardCharsets.UTF_8)
  193 + return HttpUtils.encodeUtf8(str)
193 194 .replace("+", "%20") // 加号 "+" 被替换为 "%20"
194 195 .replace("*", "%2A") // 星号 "*" 被替换为 "%2A"
195 196 .replace("%7E", "~"); // 波浪号 "%7E" 被替换为 "~"
... ...
yudao-server/src/main/resources/application.yaml
... ... @@ -175,7 +175,8 @@ spring:
175 175 azure: # OpenAI 微软
176 176 openai:
177 177 endpoint: https://eastusprejade.openai.azure.com
178   - api-key: xxx
  178 + anthropic: # Anthropic Claude
  179 + api-key: sk-muubv7cXeLw0Etgs743f365cD5Ea44429946Fa7e672d8942
179 180 ollama:
180 181 base-url: http://127.0.0.1:11434
181 182 chat:
... ... @@ -183,7 +184,7 @@ spring:
183 184 stabilityai:
184 185 api-key: sk-e53UqbboF8QJCscYvzJscJxJXoFcFg4iJjl1oqgE7baJETmx
185 186 dashscope: # 通义千问
186   - api-key: sk-71800982914041848008480000000000
  187 + api-key: sk-47aa124781be4bfb95244cc62f6xxxx
187 188 minimax: # Minimax:https://www.minimaxi.com/
188 189 api-key: xxxx
189 190 moonshot: # 月之暗灭(KIMI)
... ... @@ -193,9 +194,30 @@ spring:
193 194 chat:
194 195 options:
195 196 model: deepseek-chat
  197 + model:
  198 + rerank: false # 是否开启“通义千问”的 Rerank 模型,填写 dashscope 开启
  199 + mcp:
  200 + server:
  201 + enabled: true
  202 + name: yudao-mcp-server
  203 + version: 1.0.0
  204 + instructions: 一个 MCP 示例服务
  205 + sse-endpoint: /sse
  206 + client:
  207 + enabled: true
  208 + name: mcp
  209 + sse:
  210 + connections:
  211 + filesystem:
  212 + url: http://127.0.0.1:8089
  213 + sse-endpoint: /sse
196 214  
197 215 yudao:
198 216 ai:
  217 + gemini: # 谷歌 Gemini
  218 + enable: true
  219 + api-key: AIzaSyAVoBxgoFvvte820vEQMma2LKBnC98bqMQ
  220 + model: gemini-2.5-flash
199 221 doubao: # 字节豆包
200 222 enable: true
201 223 api-key: 5c1b5747-26d2-4ebd-a4e0-dd0e8d8b4272
... ... @@ -212,7 +234,7 @@ yudao:
212 234 enable: true
213 235 appKey: 75b161ed2aef4719b275d6e7f2a4d4cd
214 236 secretKey: YWYxYWI2MTA4ODI2NGZlYTQyNjAzZTcz
215   - model: generalv3.5
  237 + model: x1
216 238 baichuan: # 百川智能
217 239 enable: true
218 240 api-key: sk-abc
... ... @@ -227,6 +249,9 @@ yudao:
227 249 enable: true
228 250 # base-url: https://suno-55ishh05u-status2xxs-projects.vercel.app
229 251 base-url: http://127.0.0.1:3001
  252 + web-search:
  253 + enable: true
  254 + api-key: sk-40500e52840f4d24b956d0b1d80d9abe
230 255  
231 256 --- #################### 芋道相关配置 ####################
232 257  
... ...