Commit ca34d1650e2e3d9d96370455eb6af3d1c212f48a

Authored by YunaiV
1 parent 6f795186

feat:【ai 大模型】联网搜索 AiWebSearchClient 封装

yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/config/AiAutoConfiguration.java
@@ -13,6 +13,8 @@ import cn.iocoder.yudao.module.ai.framework.ai.core.model.siliconflow.SiliconFlo @@ -13,6 +13,8 @@ import cn.iocoder.yudao.module.ai.framework.ai.core.model.siliconflow.SiliconFlo
13 import cn.iocoder.yudao.module.ai.framework.ai.core.model.siliconflow.SiliconFlowChatModel; 13 import cn.iocoder.yudao.module.ai.framework.ai.core.model.siliconflow.SiliconFlowChatModel;
14 import cn.iocoder.yudao.module.ai.framework.ai.core.model.suno.api.SunoApi; 14 import cn.iocoder.yudao.module.ai.framework.ai.core.model.suno.api.SunoApi;
15 import cn.iocoder.yudao.module.ai.framework.ai.core.model.xinghuo.XingHuoChatModel; 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;
16 import lombok.extern.slf4j.Slf4j; 18 import lombok.extern.slf4j.Slf4j;
17 import org.springframework.ai.deepseek.DeepSeekChatModel; 19 import org.springframework.ai.deepseek.DeepSeekChatModel;
18 import org.springframework.ai.deepseek.DeepSeekChatOptions; 20 import org.springframework.ai.deepseek.DeepSeekChatOptions;
@@ -58,11 +60,11 @@ public class AiAutoConfiguration { @@ -58,11 +60,11 @@ public class AiAutoConfiguration {
58 @Bean 60 @Bean
59 @ConditionalOnProperty(value = "yudao.ai.gemini.enable", havingValue = "true") 61 @ConditionalOnProperty(value = "yudao.ai.gemini.enable", havingValue = "true")
60 public GeminiChatModel geminiChatModel(YudaoAiProperties yudaoAiProperties) { 62 public GeminiChatModel geminiChatModel(YudaoAiProperties yudaoAiProperties) {
61 - YudaoAiProperties.GeminiProperties properties = yudaoAiProperties.getGemini(); 63 + YudaoAiProperties.Gemini properties = yudaoAiProperties.getGemini();
62 return buildGeminiChatClient(properties); 64 return buildGeminiChatClient(properties);
63 } 65 }
64 66
65 - public GeminiChatModel buildGeminiChatClient(YudaoAiProperties.GeminiProperties properties) { 67 + public GeminiChatModel buildGeminiChatClient(YudaoAiProperties.Gemini properties) {
66 if (StrUtil.isEmpty(properties.getModel())) { 68 if (StrUtil.isEmpty(properties.getModel())) {
67 properties.setModel(GeminiChatModel.MODEL_DEFAULT); 69 properties.setModel(GeminiChatModel.MODEL_DEFAULT);
68 } 70 }
@@ -86,11 +88,11 @@ public class AiAutoConfiguration { @@ -86,11 +88,11 @@ public class AiAutoConfiguration {
86 @Bean 88 @Bean
87 @ConditionalOnProperty(value = "yudao.ai.doubao.enable", havingValue = "true") 89 @ConditionalOnProperty(value = "yudao.ai.doubao.enable", havingValue = "true")
88 public DouBaoChatModel douBaoChatClient(YudaoAiProperties yudaoAiProperties) { 90 public DouBaoChatModel douBaoChatClient(YudaoAiProperties yudaoAiProperties) {
89 - YudaoAiProperties.DouBaoProperties properties = yudaoAiProperties.getDoubao(); 91 + YudaoAiProperties.DouBao properties = yudaoAiProperties.getDoubao();
90 return buildDouBaoChatClient(properties); 92 return buildDouBaoChatClient(properties);
91 } 93 }
92 94
93 - public DouBaoChatModel buildDouBaoChatClient(YudaoAiProperties.DouBaoProperties properties) { 95 + public DouBaoChatModel buildDouBaoChatClient(YudaoAiProperties.DouBao properties) {
94 if (StrUtil.isEmpty(properties.getModel())) { 96 if (StrUtil.isEmpty(properties.getModel())) {
95 properties.setModel(DouBaoChatModel.MODEL_DEFAULT); 97 properties.setModel(DouBaoChatModel.MODEL_DEFAULT);
96 } 98 }
@@ -114,11 +116,11 @@ public class AiAutoConfiguration { @@ -114,11 +116,11 @@ public class AiAutoConfiguration {
114 @Bean 116 @Bean
115 @ConditionalOnProperty(value = "yudao.ai.siliconflow.enable", havingValue = "true") 117 @ConditionalOnProperty(value = "yudao.ai.siliconflow.enable", havingValue = "true")
116 public SiliconFlowChatModel siliconFlowChatClient(YudaoAiProperties yudaoAiProperties) { 118 public SiliconFlowChatModel siliconFlowChatClient(YudaoAiProperties yudaoAiProperties) {
117 - YudaoAiProperties.SiliconFlowProperties properties = yudaoAiProperties.getSiliconflow(); 119 + YudaoAiProperties.SiliconFlow properties = yudaoAiProperties.getSiliconflow();
118 return buildSiliconFlowChatClient(properties); 120 return buildSiliconFlowChatClient(properties);
119 } 121 }
120 122
121 - public SiliconFlowChatModel buildSiliconFlowChatClient(YudaoAiProperties.SiliconFlowProperties properties) { 123 + public SiliconFlowChatModel buildSiliconFlowChatClient(YudaoAiProperties.SiliconFlow properties) {
122 if (StrUtil.isEmpty(properties.getModel())) { 124 if (StrUtil.isEmpty(properties.getModel())) {
123 properties.setModel(SiliconFlowApiConstants.MODEL_DEFAULT); 125 properties.setModel(SiliconFlowApiConstants.MODEL_DEFAULT);
124 } 126 }
@@ -141,11 +143,11 @@ public class AiAutoConfiguration { @@ -141,11 +143,11 @@ public class AiAutoConfiguration {
141 @Bean 143 @Bean
142 @ConditionalOnProperty(value = "yudao.ai.hunyuan.enable", havingValue = "true") 144 @ConditionalOnProperty(value = "yudao.ai.hunyuan.enable", havingValue = "true")
143 public HunYuanChatModel hunYuanChatClient(YudaoAiProperties yudaoAiProperties) { 145 public HunYuanChatModel hunYuanChatClient(YudaoAiProperties yudaoAiProperties) {
144 - YudaoAiProperties.HunYuanProperties properties = yudaoAiProperties.getHunyuan(); 146 + YudaoAiProperties.HunYuan properties = yudaoAiProperties.getHunyuan();
145 return buildHunYuanChatClient(properties); 147 return buildHunYuanChatClient(properties);
146 } 148 }
147 149
148 - public HunYuanChatModel buildHunYuanChatClient(YudaoAiProperties.HunYuanProperties properties) { 150 + public HunYuanChatModel buildHunYuanChatClient(YudaoAiProperties.HunYuan properties) {
149 if (StrUtil.isEmpty(properties.getModel())) { 151 if (StrUtil.isEmpty(properties.getModel())) {
150 properties.setModel(HunYuanChatModel.MODEL_DEFAULT); 152 properties.setModel(HunYuanChatModel.MODEL_DEFAULT);
151 } 153 }
@@ -176,11 +178,11 @@ public class AiAutoConfiguration { @@ -176,11 +178,11 @@ public class AiAutoConfiguration {
176 @Bean 178 @Bean
177 @ConditionalOnProperty(value = "yudao.ai.xinghuo.enable", havingValue = "true") 179 @ConditionalOnProperty(value = "yudao.ai.xinghuo.enable", havingValue = "true")
178 public XingHuoChatModel xingHuoChatClient(YudaoAiProperties yudaoAiProperties) { 180 public XingHuoChatModel xingHuoChatClient(YudaoAiProperties yudaoAiProperties) {
179 - YudaoAiProperties.XingHuoProperties properties = yudaoAiProperties.getXinghuo(); 181 + YudaoAiProperties.XingHuo properties = yudaoAiProperties.getXinghuo();
180 return buildXingHuoChatClient(properties); 182 return buildXingHuoChatClient(properties);
181 } 183 }
182 184
183 - public XingHuoChatModel buildXingHuoChatClient(YudaoAiProperties.XingHuoProperties properties) { 185 + public XingHuoChatModel buildXingHuoChatClient(YudaoAiProperties.XingHuo properties) {
184 if (StrUtil.isEmpty(properties.getModel())) { 186 if (StrUtil.isEmpty(properties.getModel())) {
185 properties.setModel(XingHuoChatModel.MODEL_DEFAULT); 187 properties.setModel(XingHuoChatModel.MODEL_DEFAULT);
186 } 188 }
@@ -208,11 +210,11 @@ public class AiAutoConfiguration { @@ -208,11 +210,11 @@ public class AiAutoConfiguration {
208 @Bean 210 @Bean
209 @ConditionalOnProperty(value = "yudao.ai.baichuan.enable", havingValue = "true") 211 @ConditionalOnProperty(value = "yudao.ai.baichuan.enable", havingValue = "true")
210 public BaiChuanChatModel baiChuanChatClient(YudaoAiProperties yudaoAiProperties) { 212 public BaiChuanChatModel baiChuanChatClient(YudaoAiProperties yudaoAiProperties) {
211 - YudaoAiProperties.BaiChuanProperties properties = yudaoAiProperties.getBaichuan(); 213 + YudaoAiProperties.BaiChuan properties = yudaoAiProperties.getBaichuan();
212 return buildBaiChuanChatClient(properties); 214 return buildBaiChuanChatClient(properties);
213 } 215 }
214 216
215 - public BaiChuanChatModel buildBaiChuanChatClient(YudaoAiProperties.BaiChuanProperties properties) { 217 + public BaiChuanChatModel buildBaiChuanChatClient(YudaoAiProperties.BaiChuan properties) {
216 if (StrUtil.isEmpty(properties.getModel())) { 218 if (StrUtil.isEmpty(properties.getModel())) {
217 properties.setModel(BaiChuanChatModel.MODEL_DEFAULT); 219 properties.setModel(BaiChuanChatModel.MODEL_DEFAULT);
218 } 220 }
@@ -235,7 +237,7 @@ public class AiAutoConfiguration { @@ -235,7 +237,7 @@ public class AiAutoConfiguration {
235 @Bean 237 @Bean
236 @ConditionalOnProperty(value = "yudao.ai.midjourney.enable", havingValue = "true") 238 @ConditionalOnProperty(value = "yudao.ai.midjourney.enable", havingValue = "true")
237 public MidjourneyApi midjourneyApi(YudaoAiProperties yudaoAiProperties) { 239 public MidjourneyApi midjourneyApi(YudaoAiProperties yudaoAiProperties) {
238 - YudaoAiProperties.MidjourneyProperties config = yudaoAiProperties.getMidjourney(); 240 + YudaoAiProperties.Midjourney config = yudaoAiProperties.getMidjourney();
239 return new MidjourneyApi(config.getBaseUrl(), config.getApiKey(), config.getNotifyUrl()); 241 return new MidjourneyApi(config.getBaseUrl(), config.getApiKey(), config.getNotifyUrl());
240 } 242 }
241 243
@@ -261,4 +263,12 @@ public class AiAutoConfiguration { @@ -261,4 +263,12 @@ public class AiAutoConfiguration {
261 return SpringUtil.getBean(ToolCallingManager.class); 263 return SpringUtil.getBean(ToolCallingManager.class);
262 } 264 }
263 265
  266 + // ========== Web Search 相关 ==========
  267 +
  268 + @Bean
  269 + @ConditionalOnProperty(value = "yudao.ai.web-search.enable", havingValue = "true")
  270 + public AiWebSearchClient webSearchClient(YudaoAiProperties yudaoAiProperties) {
  271 + return new AiBoChaWebSearchClient(yudaoAiProperties.getWebSearch().getApiKey());
  272 + }
  273 +
264 } 274 }
265 \ No newline at end of file 275 \ No newline at end of file
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/config/YudaoAiProperties.java
@@ -16,51 +16,51 @@ public class YudaoAiProperties { @@ -16,51 +16,51 @@ public class YudaoAiProperties {
16 /** 16 /**
17 * 谷歌 Gemini 17 * 谷歌 Gemini
18 */ 18 */
19 - private GeminiProperties gemini; 19 + private Gemini gemini;
20 20
21 /** 21 /**
22 * 字节豆包 22 * 字节豆包
23 */ 23 */
24 - @SuppressWarnings("SpellCheckingInspection")  
25 - private DouBaoProperties doubao; 24 + private DouBao doubao;
26 25
27 /** 26 /**
28 * 腾讯混元 27 * 腾讯混元
29 */ 28 */
30 - @SuppressWarnings("SpellCheckingInspection")  
31 - private HunYuanProperties hunyuan; 29 + private HunYuan hunyuan;
32 30
33 /** 31 /**
34 * 硅基流动 32 * 硅基流动
35 */ 33 */
36 - @SuppressWarnings("SpellCheckingInspection")  
37 - private SiliconFlowProperties siliconflow; 34 + private SiliconFlow siliconflow;
38 35
39 /** 36 /**
40 * 讯飞星火 37 * 讯飞星火
41 */ 38 */
42 - @SuppressWarnings("SpellCheckingInspection")  
43 - private XingHuoProperties xinghuo; 39 + private XingHuo xinghuo;
44 40
45 /** 41 /**
46 * 百川 42 * 百川
47 */ 43 */
48 - @SuppressWarnings("SpellCheckingInspection")  
49 - private BaiChuanProperties baichuan; 44 + private BaiChuan baichuan;
50 45
51 /** 46 /**
52 * Midjourney 绘图 47 * Midjourney 绘图
53 */ 48 */
54 - private MidjourneyProperties midjourney; 49 + private Midjourney midjourney;
55 50
56 /** 51 /**
57 * Suno 音乐 52 * Suno 音乐
58 */ 53 */
59 @SuppressWarnings("SpellCheckingInspection") 54 @SuppressWarnings("SpellCheckingInspection")
60 - private SunoProperties suno; 55 + private Suno suno;
  56 +
  57 + /**
  58 + * 网络搜索
  59 + */
  60 + private WebSearch webSearch;
61 61
62 @Data 62 @Data
63 - public static class GeminiProperties { 63 + public static class Gemini {
64 64
65 private String enable; 65 private String enable;
66 private String apiKey; 66 private String apiKey;
@@ -73,7 +73,7 @@ public class YudaoAiProperties { @@ -73,7 +73,7 @@ public class YudaoAiProperties {
73 } 73 }
74 74
75 @Data 75 @Data
76 - public static class DouBaoProperties { 76 + public static class DouBao {
77 77
78 private String enable; 78 private String enable;
79 private String apiKey; 79 private String apiKey;
@@ -86,7 +86,7 @@ public class YudaoAiProperties { @@ -86,7 +86,7 @@ public class YudaoAiProperties {
86 } 86 }
87 87
88 @Data 88 @Data
89 - public static class HunYuanProperties { 89 + public static class HunYuan {
90 90
91 private String enable; 91 private String enable;
92 private String baseUrl; 92 private String baseUrl;
@@ -100,7 +100,7 @@ public class YudaoAiProperties { @@ -100,7 +100,7 @@ public class YudaoAiProperties {
100 } 100 }
101 101
102 @Data 102 @Data
103 - public static class SiliconFlowProperties { 103 + public static class SiliconFlow {
104 104
105 private String enable; 105 private String enable;
106 private String apiKey; 106 private String apiKey;
@@ -113,7 +113,7 @@ public class YudaoAiProperties { @@ -113,7 +113,7 @@ public class YudaoAiProperties {
113 } 113 }
114 114
115 @Data 115 @Data
116 - public static class XingHuoProperties { 116 + public static class XingHuo {
117 117
118 private String enable; 118 private String enable;
119 private String appId; 119 private String appId;
@@ -128,7 +128,7 @@ public class YudaoAiProperties { @@ -128,7 +128,7 @@ public class YudaoAiProperties {
128 } 128 }
129 129
130 @Data 130 @Data
131 - public static class BaiChuanProperties { 131 + public static class BaiChuan {
132 132
133 private String enable; 133 private String enable;
134 private String apiKey; 134 private String apiKey;
@@ -141,7 +141,7 @@ public class YudaoAiProperties { @@ -141,7 +141,7 @@ public class YudaoAiProperties {
141 } 141 }
142 142
143 @Data 143 @Data
144 - public static class MidjourneyProperties { 144 + public static class Midjourney {
145 145
146 private String enable; 146 private String enable;
147 private String baseUrl; 147 private String baseUrl;
@@ -152,12 +152,21 @@ public class YudaoAiProperties { @@ -152,12 +152,21 @@ public class YudaoAiProperties {
152 } 152 }
153 153
154 @Data 154 @Data
155 - public static class SunoProperties { 155 + public static class Suno {
156 156
157 - private boolean enable = false; 157 + private boolean enable;
158 158
159 private String baseUrl; 159 private String baseUrl;
160 160
161 } 161 }
162 162
  163 + @Data
  164 + public static class WebSearch {
  165 +
  166 + private boolean enable;
  167 +
  168 + private String apiKey;
  169 +
  170 + }
  171 +
163 } 172 }
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/AiModelFactoryImpl.java
@@ -272,7 +272,7 @@ public class AiModelFactoryImpl implements AiModelFactory { @@ -272,7 +272,7 @@ public class AiModelFactoryImpl implements AiModelFactory {
272 String cacheKey = buildClientCacheKey(MidjourneyApi.class, AiPlatformEnum.MIDJOURNEY.getPlatform(), apiKey, 272 String cacheKey = buildClientCacheKey(MidjourneyApi.class, AiPlatformEnum.MIDJOURNEY.getPlatform(), apiKey,
273 url); 273 url);
274 return Singleton.get(cacheKey, (Func0<MidjourneyApi>) () -> { 274 return Singleton.get(cacheKey, (Func0<MidjourneyApi>) () -> {
275 - YudaoAiProperties.MidjourneyProperties properties = SpringUtil.getBean(YudaoAiProperties.class) 275 + YudaoAiProperties.Midjourney properties = SpringUtil.getBean(YudaoAiProperties.class)
276 .getMidjourney(); 276 .getMidjourney();
277 return new MidjourneyApi(url, apiKey, properties.getNotifyUrl()); 277 return new MidjourneyApi(url, apiKey, properties.getNotifyUrl());
278 }); 278 });
@@ -409,7 +409,7 @@ public class AiModelFactoryImpl implements AiModelFactory { @@ -409,7 +409,7 @@ public class AiModelFactoryImpl implements AiModelFactory {
409 * 可参考 {@link AiAutoConfiguration#douBaoChatClient(YudaoAiProperties)} 409 * 可参考 {@link AiAutoConfiguration#douBaoChatClient(YudaoAiProperties)}
410 */ 410 */
411 private ChatModel buildDouBaoChatModel(String apiKey) { 411 private ChatModel buildDouBaoChatModel(String apiKey) {
412 - YudaoAiProperties.DouBaoProperties properties = new YudaoAiProperties.DouBaoProperties() 412 + YudaoAiProperties.DouBao properties = new YudaoAiProperties.DouBao()
413 .setApiKey(apiKey); 413 .setApiKey(apiKey);
414 return new AiAutoConfiguration().buildDouBaoChatClient(properties); 414 return new AiAutoConfiguration().buildDouBaoChatClient(properties);
415 } 415 }
@@ -418,7 +418,7 @@ public class AiModelFactoryImpl implements AiModelFactory { @@ -418,7 +418,7 @@ public class AiModelFactoryImpl implements AiModelFactory {
418 * 可参考 {@link AiAutoConfiguration#hunYuanChatClient(YudaoAiProperties)} 418 * 可参考 {@link AiAutoConfiguration#hunYuanChatClient(YudaoAiProperties)}
419 */ 419 */
420 private ChatModel buildHunYuanChatModel(String apiKey, String url) { 420 private ChatModel buildHunYuanChatModel(String apiKey, String url) {
421 - YudaoAiProperties.HunYuanProperties properties = new YudaoAiProperties.HunYuanProperties() 421 + YudaoAiProperties.HunYuan properties = new YudaoAiProperties.HunYuan()
422 .setBaseUrl(url).setApiKey(apiKey); 422 .setBaseUrl(url).setApiKey(apiKey);
423 return new AiAutoConfiguration().buildHunYuanChatClient(properties); 423 return new AiAutoConfiguration().buildHunYuanChatClient(properties);
424 } 424 }
@@ -427,7 +427,7 @@ public class AiModelFactoryImpl implements AiModelFactory { @@ -427,7 +427,7 @@ public class AiModelFactoryImpl implements AiModelFactory {
427 * 可参考 {@link AiAutoConfiguration#siliconFlowChatClient(YudaoAiProperties)} 427 * 可参考 {@link AiAutoConfiguration#siliconFlowChatClient(YudaoAiProperties)}
428 */ 428 */
429 private ChatModel buildSiliconFlowChatModel(String apiKey) { 429 private ChatModel buildSiliconFlowChatModel(String apiKey) {
430 - YudaoAiProperties.SiliconFlowProperties properties = new YudaoAiProperties.SiliconFlowProperties() 430 + YudaoAiProperties.SiliconFlow properties = new YudaoAiProperties.SiliconFlow()
431 .setApiKey(apiKey); 431 .setApiKey(apiKey);
432 return new AiAutoConfiguration().buildSiliconFlowChatClient(properties); 432 return new AiAutoConfiguration().buildSiliconFlowChatClient(properties);
433 } 433 }
@@ -485,7 +485,7 @@ public class AiModelFactoryImpl implements AiModelFactory { @@ -485,7 +485,7 @@ public class AiModelFactoryImpl implements AiModelFactory {
485 private static XingHuoChatModel buildXingHuoChatModel(String key) { 485 private static XingHuoChatModel buildXingHuoChatModel(String key) {
486 List<String> keys = StrUtil.split(key, '|'); 486 List<String> keys = StrUtil.split(key, '|');
487 Assert.equals(keys.size(), 2, "XingHuoChatClient 的密钥需要 (appKey|secretKey) 格式"); 487 Assert.equals(keys.size(), 2, "XingHuoChatClient 的密钥需要 (appKey|secretKey) 格式");
488 - YudaoAiProperties.XingHuoProperties properties = new YudaoAiProperties.XingHuoProperties() 488 + YudaoAiProperties.XingHuo properties = new YudaoAiProperties.XingHuo()
489 .setAppKey(keys.get(0)).setSecretKey(keys.get(1)); 489 .setAppKey(keys.get(0)).setSecretKey(keys.get(1));
490 return new AiAutoConfiguration().buildXingHuoChatClient(properties); 490 return new AiAutoConfiguration().buildXingHuoChatClient(properties);
491 } 491 }
@@ -494,7 +494,7 @@ public class AiModelFactoryImpl implements AiModelFactory { @@ -494,7 +494,7 @@ public class AiModelFactoryImpl implements AiModelFactory {
494 * 可参考 {@link AiAutoConfiguration#baiChuanChatClient(YudaoAiProperties)} 494 * 可参考 {@link AiAutoConfiguration#baiChuanChatClient(YudaoAiProperties)}
495 */ 495 */
496 private BaiChuanChatModel buildBaiChuanChatModel(String apiKey) { 496 private BaiChuanChatModel buildBaiChuanChatModel(String apiKey) {
497 - YudaoAiProperties.BaiChuanProperties properties = new YudaoAiProperties.BaiChuanProperties() 497 + YudaoAiProperties.BaiChuan properties = new YudaoAiProperties.BaiChuan()
498 .setApiKey(apiKey); 498 .setApiKey(apiKey);
499 return new AiAutoConfiguration().buildBaiChuanChatClient(properties); 499 return new AiAutoConfiguration().buildBaiChuanChatClient(properties);
500 } 500 }
@@ -540,10 +540,10 @@ public class AiModelFactoryImpl implements AiModelFactory { @@ -540,10 +540,10 @@ public class AiModelFactoryImpl implements AiModelFactory {
540 } 540 }
541 541
542 /** 542 /**
543 - * 可参考 {@link AiAutoConfiguration#buildGeminiChatClient(YudaoAiProperties.GeminiProperties)} 543 + * 可参考 {@link AiAutoConfiguration#buildGeminiChatClient(YudaoAiProperties.Gemini)}
544 */ 544 */
545 private static GeminiChatModel buildGeminiChatModel(String apiKey) { 545 private static GeminiChatModel buildGeminiChatModel(String apiKey) {
546 - YudaoAiProperties.GeminiProperties properties = SpringUtil.getBean(YudaoAiProperties.class) 546 + YudaoAiProperties.Gemini properties = SpringUtil.getBean(YudaoAiProperties.class)
547 .getGemini().setApiKey(apiKey); 547 .getGemini().setApiKey(apiKey);
548 return new AiAutoConfiguration().buildGeminiChatClient(properties); 548 return new AiAutoConfiguration().buildGeminiChatClient(properties);
549 } 549 }
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 \ No newline at end of file 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/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 \ No newline at end of file 29 \ No newline at end of file
yudao-server/src/main/resources/application.yaml
@@ -234,6 +234,9 @@ yudao: @@ -234,6 +234,9 @@ yudao:
234 enable: true 234 enable: true
235 # base-url: https://suno-55ishh05u-status2xxs-projects.vercel.app 235 # base-url: https://suno-55ishh05u-status2xxs-projects.vercel.app
236 base-url: http://127.0.0.1:3001 236 base-url: http://127.0.0.1:3001
  237 + web-search:
  238 + enable: true
  239 + api-key: sk-40500e52840f4d24b956d0b1d80d9abe
237 240
238 --- #################### 芋道相关配置 #################### 241 --- #################### 芋道相关配置 ####################
239 242