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 13 import cn.iocoder.yudao.module.ai.framework.ai.core.model.siliconflow.SiliconFlowChatModel;
14 14 import cn.iocoder.yudao.module.ai.framework.ai.core.model.suno.api.SunoApi;
15 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 18 import lombok.extern.slf4j.Slf4j;
17 19 import org.springframework.ai.deepseek.DeepSeekChatModel;
18 20 import org.springframework.ai.deepseek.DeepSeekChatOptions;
... ... @@ -58,11 +60,11 @@ public class AiAutoConfiguration {
58 60 @Bean
59 61 @ConditionalOnProperty(value = "yudao.ai.gemini.enable", havingValue = "true")
60 62 public GeminiChatModel geminiChatModel(YudaoAiProperties yudaoAiProperties) {
61   - YudaoAiProperties.GeminiProperties properties = yudaoAiProperties.getGemini();
  63 + YudaoAiProperties.Gemini properties = yudaoAiProperties.getGemini();
62 64 return buildGeminiChatClient(properties);
63 65 }
64 66  
65   - public GeminiChatModel buildGeminiChatClient(YudaoAiProperties.GeminiProperties properties) {
  67 + public GeminiChatModel buildGeminiChatClient(YudaoAiProperties.Gemini properties) {
66 68 if (StrUtil.isEmpty(properties.getModel())) {
67 69 properties.setModel(GeminiChatModel.MODEL_DEFAULT);
68 70 }
... ... @@ -86,11 +88,11 @@ public class AiAutoConfiguration {
86 88 @Bean
87 89 @ConditionalOnProperty(value = "yudao.ai.doubao.enable", havingValue = "true")
88 90 public DouBaoChatModel douBaoChatClient(YudaoAiProperties yudaoAiProperties) {
89   - YudaoAiProperties.DouBaoProperties properties = yudaoAiProperties.getDoubao();
  91 + YudaoAiProperties.DouBao properties = yudaoAiProperties.getDoubao();
90 92 return buildDouBaoChatClient(properties);
91 93 }
92 94  
93   - public DouBaoChatModel buildDouBaoChatClient(YudaoAiProperties.DouBaoProperties properties) {
  95 + public DouBaoChatModel buildDouBaoChatClient(YudaoAiProperties.DouBao properties) {
94 96 if (StrUtil.isEmpty(properties.getModel())) {
95 97 properties.setModel(DouBaoChatModel.MODEL_DEFAULT);
96 98 }
... ... @@ -114,11 +116,11 @@ public class AiAutoConfiguration {
114 116 @Bean
115 117 @ConditionalOnProperty(value = "yudao.ai.siliconflow.enable", havingValue = "true")
116 118 public SiliconFlowChatModel siliconFlowChatClient(YudaoAiProperties yudaoAiProperties) {
117   - YudaoAiProperties.SiliconFlowProperties properties = yudaoAiProperties.getSiliconflow();
  119 + YudaoAiProperties.SiliconFlow properties = yudaoAiProperties.getSiliconflow();
118 120 return buildSiliconFlowChatClient(properties);
119 121 }
120 122  
121   - public SiliconFlowChatModel buildSiliconFlowChatClient(YudaoAiProperties.SiliconFlowProperties properties) {
  123 + public SiliconFlowChatModel buildSiliconFlowChatClient(YudaoAiProperties.SiliconFlow properties) {
122 124 if (StrUtil.isEmpty(properties.getModel())) {
123 125 properties.setModel(SiliconFlowApiConstants.MODEL_DEFAULT);
124 126 }
... ... @@ -141,11 +143,11 @@ public class AiAutoConfiguration {
141 143 @Bean
142 144 @ConditionalOnProperty(value = "yudao.ai.hunyuan.enable", havingValue = "true")
143 145 public HunYuanChatModel hunYuanChatClient(YudaoAiProperties yudaoAiProperties) {
144   - YudaoAiProperties.HunYuanProperties properties = yudaoAiProperties.getHunyuan();
  146 + YudaoAiProperties.HunYuan properties = yudaoAiProperties.getHunyuan();
145 147 return buildHunYuanChatClient(properties);
146 148 }
147 149  
148   - public HunYuanChatModel buildHunYuanChatClient(YudaoAiProperties.HunYuanProperties properties) {
  150 + public HunYuanChatModel buildHunYuanChatClient(YudaoAiProperties.HunYuan properties) {
149 151 if (StrUtil.isEmpty(properties.getModel())) {
150 152 properties.setModel(HunYuanChatModel.MODEL_DEFAULT);
151 153 }
... ... @@ -176,11 +178,11 @@ public class AiAutoConfiguration {
176 178 @Bean
177 179 @ConditionalOnProperty(value = "yudao.ai.xinghuo.enable", havingValue = "true")
178 180 public XingHuoChatModel xingHuoChatClient(YudaoAiProperties yudaoAiProperties) {
179   - YudaoAiProperties.XingHuoProperties properties = yudaoAiProperties.getXinghuo();
  181 + YudaoAiProperties.XingHuo properties = yudaoAiProperties.getXinghuo();
180 182 return buildXingHuoChatClient(properties);
181 183 }
182 184  
183   - public XingHuoChatModel buildXingHuoChatClient(YudaoAiProperties.XingHuoProperties properties) {
  185 + public XingHuoChatModel buildXingHuoChatClient(YudaoAiProperties.XingHuo properties) {
184 186 if (StrUtil.isEmpty(properties.getModel())) {
185 187 properties.setModel(XingHuoChatModel.MODEL_DEFAULT);
186 188 }
... ... @@ -208,11 +210,11 @@ public class AiAutoConfiguration {
208 210 @Bean
209 211 @ConditionalOnProperty(value = "yudao.ai.baichuan.enable", havingValue = "true")
210 212 public BaiChuanChatModel baiChuanChatClient(YudaoAiProperties yudaoAiProperties) {
211   - YudaoAiProperties.BaiChuanProperties properties = yudaoAiProperties.getBaichuan();
  213 + YudaoAiProperties.BaiChuan properties = yudaoAiProperties.getBaichuan();
212 214 return buildBaiChuanChatClient(properties);
213 215 }
214 216  
215   - public BaiChuanChatModel buildBaiChuanChatClient(YudaoAiProperties.BaiChuanProperties properties) {
  217 + public BaiChuanChatModel buildBaiChuanChatClient(YudaoAiProperties.BaiChuan properties) {
216 218 if (StrUtil.isEmpty(properties.getModel())) {
217 219 properties.setModel(BaiChuanChatModel.MODEL_DEFAULT);
218 220 }
... ... @@ -235,7 +237,7 @@ public class AiAutoConfiguration {
235 237 @Bean
236 238 @ConditionalOnProperty(value = "yudao.ai.midjourney.enable", havingValue = "true")
237 239 public MidjourneyApi midjourneyApi(YudaoAiProperties yudaoAiProperties) {
238   - YudaoAiProperties.MidjourneyProperties config = yudaoAiProperties.getMidjourney();
  240 + YudaoAiProperties.Midjourney config = yudaoAiProperties.getMidjourney();
239 241 return new MidjourneyApi(config.getBaseUrl(), config.getApiKey(), config.getNotifyUrl());
240 242 }
241 243  
... ... @@ -261,4 +263,12 @@ public class AiAutoConfiguration {
261 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 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 16 /**
17 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 47 * Midjourney 绘图
53 48 */
54   - private MidjourneyProperties midjourney;
  49 + private Midjourney midjourney;
55 50  
56 51 /**
57 52 * Suno 音乐
58 53 */
59 54 @SuppressWarnings("SpellCheckingInspection")
60   - private SunoProperties suno;
  55 + private Suno suno;
  56 +
  57 + /**
  58 + * 网络搜索
  59 + */
  60 + private WebSearch webSearch;
61 61  
62 62 @Data
63   - public static class GeminiProperties {
  63 + public static class Gemini {
64 64  
65 65 private String enable;
66 66 private String apiKey;
... ... @@ -73,7 +73,7 @@ public class YudaoAiProperties {
73 73 }
74 74  
75 75 @Data
76   - public static class DouBaoProperties {
  76 + public static class DouBao {
77 77  
78 78 private String enable;
79 79 private String apiKey;
... ... @@ -86,7 +86,7 @@ public class YudaoAiProperties {
86 86 }
87 87  
88 88 @Data
89   - public static class HunYuanProperties {
  89 + public static class HunYuan {
90 90  
91 91 private String enable;
92 92 private String baseUrl;
... ... @@ -100,7 +100,7 @@ public class YudaoAiProperties {
100 100 }
101 101  
102 102 @Data
103   - public static class SiliconFlowProperties {
  103 + public static class SiliconFlow {
104 104  
105 105 private String enable;
106 106 private String apiKey;
... ... @@ -113,7 +113,7 @@ public class YudaoAiProperties {
113 113 }
114 114  
115 115 @Data
116   - public static class XingHuoProperties {
  116 + public static class XingHuo {
117 117  
118 118 private String enable;
119 119 private String appId;
... ... @@ -128,7 +128,7 @@ public class YudaoAiProperties {
128 128 }
129 129  
130 130 @Data
131   - public static class BaiChuanProperties {
  131 + public static class BaiChuan {
132 132  
133 133 private String enable;
134 134 private String apiKey;
... ... @@ -141,7 +141,7 @@ public class YudaoAiProperties {
141 141 }
142 142  
143 143 @Data
144   - public static class MidjourneyProperties {
  144 + public static class Midjourney {
145 145  
146 146 private String enable;
147 147 private String baseUrl;
... ... @@ -152,12 +152,21 @@ public class YudaoAiProperties {
152 152 }
153 153  
154 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 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 272 String cacheKey = buildClientCacheKey(MidjourneyApi.class, AiPlatformEnum.MIDJOURNEY.getPlatform(), apiKey,
273 273 url);
274 274 return Singleton.get(cacheKey, (Func0<MidjourneyApi>) () -> {
275   - YudaoAiProperties.MidjourneyProperties properties = SpringUtil.getBean(YudaoAiProperties.class)
  275 + YudaoAiProperties.Midjourney properties = SpringUtil.getBean(YudaoAiProperties.class)
276 276 .getMidjourney();
277 277 return new MidjourneyApi(url, apiKey, properties.getNotifyUrl());
278 278 });
... ... @@ -409,7 +409,7 @@ public class AiModelFactoryImpl implements AiModelFactory {
409 409 * 可参考 {@link AiAutoConfiguration#douBaoChatClient(YudaoAiProperties)}
410 410 */
411 411 private ChatModel buildDouBaoChatModel(String apiKey) {
412   - YudaoAiProperties.DouBaoProperties properties = new YudaoAiProperties.DouBaoProperties()
  412 + YudaoAiProperties.DouBao properties = new YudaoAiProperties.DouBao()
413 413 .setApiKey(apiKey);
414 414 return new AiAutoConfiguration().buildDouBaoChatClient(properties);
415 415 }
... ... @@ -418,7 +418,7 @@ public class AiModelFactoryImpl implements AiModelFactory {
418 418 * 可参考 {@link AiAutoConfiguration#hunYuanChatClient(YudaoAiProperties)}
419 419 */
420 420 private ChatModel buildHunYuanChatModel(String apiKey, String url) {
421   - YudaoAiProperties.HunYuanProperties properties = new YudaoAiProperties.HunYuanProperties()
  421 + YudaoAiProperties.HunYuan properties = new YudaoAiProperties.HunYuan()
422 422 .setBaseUrl(url).setApiKey(apiKey);
423 423 return new AiAutoConfiguration().buildHunYuanChatClient(properties);
424 424 }
... ... @@ -427,7 +427,7 @@ public class AiModelFactoryImpl implements AiModelFactory {
427 427 * 可参考 {@link AiAutoConfiguration#siliconFlowChatClient(YudaoAiProperties)}
428 428 */
429 429 private ChatModel buildSiliconFlowChatModel(String apiKey) {
430   - YudaoAiProperties.SiliconFlowProperties properties = new YudaoAiProperties.SiliconFlowProperties()
  430 + YudaoAiProperties.SiliconFlow properties = new YudaoAiProperties.SiliconFlow()
431 431 .setApiKey(apiKey);
432 432 return new AiAutoConfiguration().buildSiliconFlowChatClient(properties);
433 433 }
... ... @@ -485,7 +485,7 @@ public class AiModelFactoryImpl implements AiModelFactory {
485 485 private static XingHuoChatModel buildXingHuoChatModel(String key) {
486 486 List<String> keys = StrUtil.split(key, '|');
487 487 Assert.equals(keys.size(), 2, "XingHuoChatClient 的密钥需要 (appKey|secretKey) 格式");
488   - YudaoAiProperties.XingHuoProperties properties = new YudaoAiProperties.XingHuoProperties()
  488 + YudaoAiProperties.XingHuo properties = new YudaoAiProperties.XingHuo()
489 489 .setAppKey(keys.get(0)).setSecretKey(keys.get(1));
490 490 return new AiAutoConfiguration().buildXingHuoChatClient(properties);
491 491 }
... ... @@ -494,7 +494,7 @@ public class AiModelFactoryImpl implements AiModelFactory {
494 494 * 可参考 {@link AiAutoConfiguration#baiChuanChatClient(YudaoAiProperties)}
495 495 */
496 496 private BaiChuanChatModel buildBaiChuanChatModel(String apiKey) {
497   - YudaoAiProperties.BaiChuanProperties properties = new YudaoAiProperties.BaiChuanProperties()
  497 + YudaoAiProperties.BaiChuan properties = new YudaoAiProperties.BaiChuan()
498 498 .setApiKey(apiKey);
499 499 return new AiAutoConfiguration().buildBaiChuanChatClient(properties);
500 500 }
... ... @@ -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 545 private static GeminiChatModel buildGeminiChatModel(String apiKey) {
546   - YudaoAiProperties.GeminiProperties properties = SpringUtil.getBean(YudaoAiProperties.class)
  546 + YudaoAiProperties.Gemini properties = SpringUtil.getBean(YudaoAiProperties.class)
547 547 .getGemini().setApiKey(apiKey);
548 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 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 29 \ No newline at end of file
... ...
yudao-server/src/main/resources/application.yaml
... ... @@ -234,6 +234,9 @@ yudao:
234 234 enable: true
235 235 # base-url: https://suno-55ishh05u-status2xxs-projects.vercel.app
236 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  
... ...