From ca34d1650e2e3d9d96370455eb6af3d1c212f48a Mon Sep 17 00:00:00 2001 From: YunaiV Date: Mon, 25 Aug 2025 22:23:03 +0800 Subject: [PATCH] =?UTF-8?q?feat=EF=BC=9A=E3=80=90ai=20=E5=A4=A7=E6=A8=A1?= =?UTF-8?q?=E5=9E=8B=E3=80=91=E8=81=94=E7=BD=91=E6=90=9C=E7=B4=A2=20AiWebS?= =?UTF-8?q?earchClient=20=E5=B0=81=E8=A3=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ai/config/AiAutoConfiguration.java | 36 +++-- .../ai/config/YudaoAiProperties.java | 53 +++--- .../ai/core/model/AiModelFactoryImpl.java | 16 +- .../ai/core/webserch/AiWebSearchClient.java | 18 +++ .../ai/core/webserch/AiWebSearchRequest.java | 34 ++++ .../ai/core/webserch/AiWebSearchResponse.java | 62 +++++++ .../bocha/AiBoChaWebSearchClient.java | 153 ++++++++++++++++++ .../websearch/AiBoChaWebSearchClientTest.java | 28 ++++ .../src/main/resources/application.yaml | 3 + 9 files changed, 360 insertions(+), 43 deletions(-) create mode 100644 yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/webserch/AiWebSearchClient.java create mode 100644 yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/webserch/AiWebSearchRequest.java create mode 100644 yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/webserch/AiWebSearchResponse.java create mode 100644 yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/webserch/bocha/AiBoChaWebSearchClient.java create mode 100644 yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/websearch/AiBoChaWebSearchClientTest.java diff --git a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/config/AiAutoConfiguration.java b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/config/AiAutoConfiguration.java index 2d63df311a..6fd62bcabd 100644 --- a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/config/AiAutoConfiguration.java +++ b/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 import cn.iocoder.yudao.module.ai.framework.ai.core.model.siliconflow.SiliconFlowChatModel; import cn.iocoder.yudao.module.ai.framework.ai.core.model.suno.api.SunoApi; import cn.iocoder.yudao.module.ai.framework.ai.core.model.xinghuo.XingHuoChatModel; +import cn.iocoder.yudao.module.ai.framework.ai.core.webserch.AiWebSearchClient; +import cn.iocoder.yudao.module.ai.framework.ai.core.webserch.bocha.AiBoChaWebSearchClient; import lombok.extern.slf4j.Slf4j; import org.springframework.ai.deepseek.DeepSeekChatModel; import org.springframework.ai.deepseek.DeepSeekChatOptions; @@ -58,11 +60,11 @@ public class AiAutoConfiguration { @Bean @ConditionalOnProperty(value = "yudao.ai.gemini.enable", havingValue = "true") public GeminiChatModel geminiChatModel(YudaoAiProperties yudaoAiProperties) { - YudaoAiProperties.GeminiProperties properties = yudaoAiProperties.getGemini(); + YudaoAiProperties.Gemini properties = yudaoAiProperties.getGemini(); return buildGeminiChatClient(properties); } - public GeminiChatModel buildGeminiChatClient(YudaoAiProperties.GeminiProperties properties) { + public GeminiChatModel buildGeminiChatClient(YudaoAiProperties.Gemini properties) { if (StrUtil.isEmpty(properties.getModel())) { properties.setModel(GeminiChatModel.MODEL_DEFAULT); } @@ -86,11 +88,11 @@ public class AiAutoConfiguration { @Bean @ConditionalOnProperty(value = "yudao.ai.doubao.enable", havingValue = "true") public DouBaoChatModel douBaoChatClient(YudaoAiProperties yudaoAiProperties) { - YudaoAiProperties.DouBaoProperties properties = yudaoAiProperties.getDoubao(); + YudaoAiProperties.DouBao properties = yudaoAiProperties.getDoubao(); return buildDouBaoChatClient(properties); } - public DouBaoChatModel buildDouBaoChatClient(YudaoAiProperties.DouBaoProperties properties) { + public DouBaoChatModel buildDouBaoChatClient(YudaoAiProperties.DouBao properties) { if (StrUtil.isEmpty(properties.getModel())) { properties.setModel(DouBaoChatModel.MODEL_DEFAULT); } @@ -114,11 +116,11 @@ public class AiAutoConfiguration { @Bean @ConditionalOnProperty(value = "yudao.ai.siliconflow.enable", havingValue = "true") public SiliconFlowChatModel siliconFlowChatClient(YudaoAiProperties yudaoAiProperties) { - YudaoAiProperties.SiliconFlowProperties properties = yudaoAiProperties.getSiliconflow(); + YudaoAiProperties.SiliconFlow properties = yudaoAiProperties.getSiliconflow(); return buildSiliconFlowChatClient(properties); } - public SiliconFlowChatModel buildSiliconFlowChatClient(YudaoAiProperties.SiliconFlowProperties properties) { + public SiliconFlowChatModel buildSiliconFlowChatClient(YudaoAiProperties.SiliconFlow properties) { if (StrUtil.isEmpty(properties.getModel())) { properties.setModel(SiliconFlowApiConstants.MODEL_DEFAULT); } @@ -141,11 +143,11 @@ public class AiAutoConfiguration { @Bean @ConditionalOnProperty(value = "yudao.ai.hunyuan.enable", havingValue = "true") public HunYuanChatModel hunYuanChatClient(YudaoAiProperties yudaoAiProperties) { - YudaoAiProperties.HunYuanProperties properties = yudaoAiProperties.getHunyuan(); + YudaoAiProperties.HunYuan properties = yudaoAiProperties.getHunyuan(); return buildHunYuanChatClient(properties); } - public HunYuanChatModel buildHunYuanChatClient(YudaoAiProperties.HunYuanProperties properties) { + public HunYuanChatModel buildHunYuanChatClient(YudaoAiProperties.HunYuan properties) { if (StrUtil.isEmpty(properties.getModel())) { properties.setModel(HunYuanChatModel.MODEL_DEFAULT); } @@ -176,11 +178,11 @@ public class AiAutoConfiguration { @Bean @ConditionalOnProperty(value = "yudao.ai.xinghuo.enable", havingValue = "true") public XingHuoChatModel xingHuoChatClient(YudaoAiProperties yudaoAiProperties) { - YudaoAiProperties.XingHuoProperties properties = yudaoAiProperties.getXinghuo(); + YudaoAiProperties.XingHuo properties = yudaoAiProperties.getXinghuo(); return buildXingHuoChatClient(properties); } - public XingHuoChatModel buildXingHuoChatClient(YudaoAiProperties.XingHuoProperties properties) { + public XingHuoChatModel buildXingHuoChatClient(YudaoAiProperties.XingHuo properties) { if (StrUtil.isEmpty(properties.getModel())) { properties.setModel(XingHuoChatModel.MODEL_DEFAULT); } @@ -208,11 +210,11 @@ public class AiAutoConfiguration { @Bean @ConditionalOnProperty(value = "yudao.ai.baichuan.enable", havingValue = "true") public BaiChuanChatModel baiChuanChatClient(YudaoAiProperties yudaoAiProperties) { - YudaoAiProperties.BaiChuanProperties properties = yudaoAiProperties.getBaichuan(); + YudaoAiProperties.BaiChuan properties = yudaoAiProperties.getBaichuan(); return buildBaiChuanChatClient(properties); } - public BaiChuanChatModel buildBaiChuanChatClient(YudaoAiProperties.BaiChuanProperties properties) { + public BaiChuanChatModel buildBaiChuanChatClient(YudaoAiProperties.BaiChuan properties) { if (StrUtil.isEmpty(properties.getModel())) { properties.setModel(BaiChuanChatModel.MODEL_DEFAULT); } @@ -235,7 +237,7 @@ public class AiAutoConfiguration { @Bean @ConditionalOnProperty(value = "yudao.ai.midjourney.enable", havingValue = "true") public MidjourneyApi midjourneyApi(YudaoAiProperties yudaoAiProperties) { - YudaoAiProperties.MidjourneyProperties config = yudaoAiProperties.getMidjourney(); + YudaoAiProperties.Midjourney config = yudaoAiProperties.getMidjourney(); return new MidjourneyApi(config.getBaseUrl(), config.getApiKey(), config.getNotifyUrl()); } @@ -261,4 +263,12 @@ public class AiAutoConfiguration { return SpringUtil.getBean(ToolCallingManager.class); } + // ========== Web Search 相关 ========== + + @Bean + @ConditionalOnProperty(value = "yudao.ai.web-search.enable", havingValue = "true") + public AiWebSearchClient webSearchClient(YudaoAiProperties yudaoAiProperties) { + return new AiBoChaWebSearchClient(yudaoAiProperties.getWebSearch().getApiKey()); + } + } \ No newline at end of file diff --git a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/config/YudaoAiProperties.java b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/config/YudaoAiProperties.java index 9c028c6cf0..67d3bb5f3a 100644 --- a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/config/YudaoAiProperties.java +++ b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/config/YudaoAiProperties.java @@ -16,51 +16,51 @@ public class YudaoAiProperties { /** * 谷歌 Gemini */ - private GeminiProperties gemini; + private Gemini gemini; /** * 字节豆包 */ - @SuppressWarnings("SpellCheckingInspection") - private DouBaoProperties doubao; + private DouBao doubao; /** * 腾讯混元 */ - @SuppressWarnings("SpellCheckingInspection") - private HunYuanProperties hunyuan; + private HunYuan hunyuan; /** * 硅基流动 */ - @SuppressWarnings("SpellCheckingInspection") - private SiliconFlowProperties siliconflow; + private SiliconFlow siliconflow; /** * 讯飞星火 */ - @SuppressWarnings("SpellCheckingInspection") - private XingHuoProperties xinghuo; + private XingHuo xinghuo; /** * 百川 */ - @SuppressWarnings("SpellCheckingInspection") - private BaiChuanProperties baichuan; + private BaiChuan baichuan; /** * Midjourney 绘图 */ - private MidjourneyProperties midjourney; + private Midjourney midjourney; /** * Suno 音乐 */ @SuppressWarnings("SpellCheckingInspection") - private SunoProperties suno; + private Suno suno; + + /** + * 网络搜索 + */ + private WebSearch webSearch; @Data - public static class GeminiProperties { + public static class Gemini { private String enable; private String apiKey; @@ -73,7 +73,7 @@ public class YudaoAiProperties { } @Data - public static class DouBaoProperties { + public static class DouBao { private String enable; private String apiKey; @@ -86,7 +86,7 @@ public class YudaoAiProperties { } @Data - public static class HunYuanProperties { + public static class HunYuan { private String enable; private String baseUrl; @@ -100,7 +100,7 @@ public class YudaoAiProperties { } @Data - public static class SiliconFlowProperties { + public static class SiliconFlow { private String enable; private String apiKey; @@ -113,7 +113,7 @@ public class YudaoAiProperties { } @Data - public static class XingHuoProperties { + public static class XingHuo { private String enable; private String appId; @@ -128,7 +128,7 @@ public class YudaoAiProperties { } @Data - public static class BaiChuanProperties { + public static class BaiChuan { private String enable; private String apiKey; @@ -141,7 +141,7 @@ public class YudaoAiProperties { } @Data - public static class MidjourneyProperties { + public static class Midjourney { private String enable; private String baseUrl; @@ -152,12 +152,21 @@ public class YudaoAiProperties { } @Data - public static class SunoProperties { + public static class Suno { - private boolean enable = false; + private boolean enable; private String baseUrl; } + @Data + public static class WebSearch { + + private boolean enable; + + private String apiKey; + + } + } diff --git a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/AiModelFactoryImpl.java b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/AiModelFactoryImpl.java index 03ebb312d9..75798ebd2a 100644 --- a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/AiModelFactoryImpl.java +++ b/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 { String cacheKey = buildClientCacheKey(MidjourneyApi.class, AiPlatformEnum.MIDJOURNEY.getPlatform(), apiKey, url); return Singleton.get(cacheKey, (Func0) () -> { - YudaoAiProperties.MidjourneyProperties properties = SpringUtil.getBean(YudaoAiProperties.class) + YudaoAiProperties.Midjourney properties = SpringUtil.getBean(YudaoAiProperties.class) .getMidjourney(); return new MidjourneyApi(url, apiKey, properties.getNotifyUrl()); }); @@ -409,7 +409,7 @@ public class AiModelFactoryImpl implements AiModelFactory { * 可参考 {@link AiAutoConfiguration#douBaoChatClient(YudaoAiProperties)} */ private ChatModel buildDouBaoChatModel(String apiKey) { - YudaoAiProperties.DouBaoProperties properties = new YudaoAiProperties.DouBaoProperties() + YudaoAiProperties.DouBao properties = new YudaoAiProperties.DouBao() .setApiKey(apiKey); return new AiAutoConfiguration().buildDouBaoChatClient(properties); } @@ -418,7 +418,7 @@ public class AiModelFactoryImpl implements AiModelFactory { * 可参考 {@link AiAutoConfiguration#hunYuanChatClient(YudaoAiProperties)} */ private ChatModel buildHunYuanChatModel(String apiKey, String url) { - YudaoAiProperties.HunYuanProperties properties = new YudaoAiProperties.HunYuanProperties() + YudaoAiProperties.HunYuan properties = new YudaoAiProperties.HunYuan() .setBaseUrl(url).setApiKey(apiKey); return new AiAutoConfiguration().buildHunYuanChatClient(properties); } @@ -427,7 +427,7 @@ public class AiModelFactoryImpl implements AiModelFactory { * 可参考 {@link AiAutoConfiguration#siliconFlowChatClient(YudaoAiProperties)} */ private ChatModel buildSiliconFlowChatModel(String apiKey) { - YudaoAiProperties.SiliconFlowProperties properties = new YudaoAiProperties.SiliconFlowProperties() + YudaoAiProperties.SiliconFlow properties = new YudaoAiProperties.SiliconFlow() .setApiKey(apiKey); return new AiAutoConfiguration().buildSiliconFlowChatClient(properties); } @@ -485,7 +485,7 @@ public class AiModelFactoryImpl implements AiModelFactory { private static XingHuoChatModel buildXingHuoChatModel(String key) { List keys = StrUtil.split(key, '|'); Assert.equals(keys.size(), 2, "XingHuoChatClient 的密钥需要 (appKey|secretKey) 格式"); - YudaoAiProperties.XingHuoProperties properties = new YudaoAiProperties.XingHuoProperties() + YudaoAiProperties.XingHuo properties = new YudaoAiProperties.XingHuo() .setAppKey(keys.get(0)).setSecretKey(keys.get(1)); return new AiAutoConfiguration().buildXingHuoChatClient(properties); } @@ -494,7 +494,7 @@ public class AiModelFactoryImpl implements AiModelFactory { * 可参考 {@link AiAutoConfiguration#baiChuanChatClient(YudaoAiProperties)} */ private BaiChuanChatModel buildBaiChuanChatModel(String apiKey) { - YudaoAiProperties.BaiChuanProperties properties = new YudaoAiProperties.BaiChuanProperties() + YudaoAiProperties.BaiChuan properties = new YudaoAiProperties.BaiChuan() .setApiKey(apiKey); return new AiAutoConfiguration().buildBaiChuanChatClient(properties); } @@ -540,10 +540,10 @@ public class AiModelFactoryImpl implements AiModelFactory { } /** - * 可参考 {@link AiAutoConfiguration#buildGeminiChatClient(YudaoAiProperties.GeminiProperties)} + * 可参考 {@link AiAutoConfiguration#buildGeminiChatClient(YudaoAiProperties.Gemini)} */ private static GeminiChatModel buildGeminiChatModel(String apiKey) { - YudaoAiProperties.GeminiProperties properties = SpringUtil.getBean(YudaoAiProperties.class) + YudaoAiProperties.Gemini properties = SpringUtil.getBean(YudaoAiProperties.class) .getGemini().setApiKey(apiKey); return new AiAutoConfiguration().buildGeminiChatClient(properties); } diff --git a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/webserch/AiWebSearchClient.java b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/webserch/AiWebSearchClient.java new file mode 100644 index 0000000000..9fbff556c1 --- /dev/null +++ b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/webserch/AiWebSearchClient.java @@ -0,0 +1,18 @@ +package cn.iocoder.yudao.module.ai.framework.ai.core.webserch; + +/** + * 网络搜索客户端接口 + * + * @author 芋道源码 + */ +public interface AiWebSearchClient { + + /** + * 网页搜索 + * + * @param request 搜索请求 + * @return 搜索结果 + */ + AiWebSearchResponse search(AiWebSearchRequest request); + +} diff --git a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/webserch/AiWebSearchRequest.java b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/webserch/AiWebSearchRequest.java new file mode 100644 index 0000000000..9bd2cfef32 --- /dev/null +++ b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/webserch/AiWebSearchRequest.java @@ -0,0 +1,34 @@ +package cn.iocoder.yudao.module.ai.framework.ai.core.webserch; + +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +@Data +public class AiWebSearchRequest { + + /** + * 用户的搜索词 + */ + @NotEmpty(message = "搜索词不能为空") + private String query; + + /** + * 是否显示文本摘要 + * + * true - 显示 + * false - 不显示(默认) + */ + private Boolean summary; + + /** + * 返回结果的条数 + */ + @NotNull(message = "返回结果条数不能为空") + @Min(message = "返回结果条数最小为 1", value = 1) + @Max(message = "返回结果条数最大为 50", value = 50) + private Integer count; + +} diff --git a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/webserch/AiWebSearchResponse.java b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/webserch/AiWebSearchResponse.java new file mode 100644 index 0000000000..8755b32ed0 --- /dev/null +++ b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/webserch/AiWebSearchResponse.java @@ -0,0 +1,62 @@ +package cn.iocoder.yudao.module.ai.framework.ai.core.webserch; + +import lombok.Data; + +import java.util.List; + +@Data +public class AiWebSearchResponse { + + /** + * 总数(总共匹配的网页数) + */ + private Long total; + + /** + * 数据列表 + */ + private List lists; + + /** + * 网页对象 + */ + @Data + public static class WebPage { + + /** + * 名称 + * + * 例如说:搜狐网 + */ + private String name; + /** + * 图标 + */ + private String icon; + + /** + * 标题 + * + * 例如说:186页|阿里巴巴:2024年环境、社会和治理(ESG)报告 + */ + private String title; + /** + * URL + * + * 例如说:https://m.sohu.com/a/815036254_121819701/?pvid=000115_3w_a + */ + @SuppressWarnings("JavadocLinkAsPlainText") + private String url; + + /** + * 内容的简短描述 + */ + private String snippet; + /** + * 内容的文本摘要 + */ + private String summary; + + } + +} \ No newline at end of file diff --git a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/webserch/bocha/AiBoChaWebSearchClient.java b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/webserch/bocha/AiBoChaWebSearchClient.java new file mode 100644 index 0000000000..7395fe645a --- /dev/null +++ b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/webserch/bocha/AiBoChaWebSearchClient.java @@ -0,0 +1,153 @@ +package cn.iocoder.yudao.module.ai.framework.ai.core.webserch.bocha; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.lang.Assert; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.module.ai.framework.ai.core.webserch.AiWebSearchClient; +import cn.iocoder.yudao.module.ai.framework.ai.core.webserch.AiWebSearchRequest; +import cn.iocoder.yudao.module.ai.framework.ai.core.webserch.AiWebSearchResponse; +import com.fasterxml.jackson.annotation.JsonInclude; +import lombok.extern.slf4j.Slf4j; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.http.HttpStatusCode; +import org.springframework.http.MediaType; +import org.springframework.web.reactive.function.client.ClientResponse; +import org.springframework.web.reactive.function.client.WebClient; +import reactor.core.publisher.Mono; + +import java.util.List; +import java.util.function.Function; +import java.util.function.Predicate; + +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; + +/** + * 博查 {@link AiWebSearchClient} 实现类 + * + * @see 博查 AI 开放平台 + * + * @author 芋道源码 + */ +@Slf4j +public class AiBoChaWebSearchClient implements AiWebSearchClient { + + public static final String BASE_URL = "https://api.bochaai.com"; + private static final String AUTHORIZATION_HEADER = "Authorization"; + private static final String BEARER_PREFIX = "Bearer "; + + private final WebClient webClient; + + private final Predicate STATUS_PREDICATE = status -> !status.is2xxSuccessful(); + + private final Function>> EXCEPTION_FUNCTION = + reqParam -> response -> response.bodyToMono(String.class).handle((responseBody, sink) -> { + log.error("[AiBoChaWebSearchClient] 调用失败!请求参数:[{}],响应数据: [{}]", reqParam, responseBody); + sink.error(new IllegalStateException("[AiBoChaWebSearchClient] 调用失败!")); + }); + + public AiBoChaWebSearchClient(String apiKey) { + this.webClient = WebClient.builder() + .baseUrl(BASE_URL) + .defaultHeaders((headers) -> { + headers.setContentType(MediaType.APPLICATION_JSON); + headers.add(AUTHORIZATION_HEADER, BEARER_PREFIX + apiKey); + }) + .build(); + } + + @Override + public AiWebSearchResponse search(AiWebSearchRequest request) { + // 转换请求参数 + WebSearchRequest webSearchRequest = new WebSearchRequest( + request.getQuery(), + request.getSummary(), + request.getCount() + ); + // 调用博查 API + CommonResult response = this.webClient.post() + .uri("/v1/web-search") + .bodyValue(webSearchRequest) + .retrieve() + .onStatus(STATUS_PREDICATE, EXCEPTION_FUNCTION.apply(webSearchRequest)) + .bodyToMono(new ParameterizedTypeReference>() {}) + .block(); + if (response == null) { + throw new IllegalStateException("[search][搜索结果为空]"); + } + if (response.getData() == null) { + throw new IllegalStateException(String.format("[search][搜索失败,code = %s, msg = %s]", + response.getCode(), response.getMsg())); + } + WebSearchResponse data = response.getData(); + + // 转换结果 + AiWebSearchResponse result = new AiWebSearchResponse(); + if (data.webPages() == null || CollUtil.isEmpty(data.webPages().value())) { + return result.setTotal(0L).setLists(List.of()); + } + return result.setTotal(data.webPages().totalEstimatedMatches()) + .setLists(convertList(data.webPages().value(), page -> new AiWebSearchResponse.WebPage() + .setName(page.siteName()).setIcon(page.siteIcon()) + .setTitle(page.name()).setUrl(page.url()) + .setSnippet(page.snippet()).setSummary(page.summary()))); + } + + /** + * 网页搜索请求参数 + */ + @JsonInclude(value = JsonInclude.Include.NON_NULL) + public record WebSearchRequest( + String query, + Boolean summary, + Integer count + ) { + public WebSearchRequest { + Assert.notBlank(query, "query 不能为空"); + } + } + + /** + * 网页搜索响应 + */ + @JsonInclude(value = JsonInclude.Include.NON_NULL) + public record WebSearchResponse( + WebSearchWebPages webPages + ) { + } + + /** + * 网页搜索结果 + */ + @JsonInclude(value = JsonInclude.Include.NON_NULL) + public record WebSearchWebPages( + String webSearchUrl, + Long totalEstimatedMatches, + List value, + Boolean someResultsRemoved + ) { + + /** + * 网页结果值 + */ + @JsonInclude(value = JsonInclude.Include.NON_NULL) + public record WebPageValue( + String id, + String name, + String url, + String displayUrl, + String snippet, + String summary, + String siteName, + String siteIcon, + String datePublished, + String dateLastCrawled, + String cachedPageUrl, + String language, + Boolean isFamilyFriendly, + Boolean isNavigational + ) { + } + + } + +} diff --git a/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/websearch/AiBoChaWebSearchClientTest.java b/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/websearch/AiBoChaWebSearchClientTest.java new file mode 100644 index 0000000000..0a02ab589d --- /dev/null +++ b/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/websearch/AiBoChaWebSearchClientTest.java @@ -0,0 +1,28 @@ +package cn.iocoder.yudao.module.ai.framework.ai.core.websearch; + +import cn.iocoder.yudao.framework.common.util.json.JsonUtils; +import cn.iocoder.yudao.module.ai.framework.ai.core.webserch.AiWebSearchRequest; +import cn.iocoder.yudao.module.ai.framework.ai.core.webserch.AiWebSearchResponse; +import cn.iocoder.yudao.module.ai.framework.ai.core.webserch.bocha.AiBoChaWebSearchClient; +import org.junit.jupiter.api.Test; + +/** + * {@link AiBoChaWebSearchClient} 集成测试类 + * + * @author 芋道源码 + */ +public class AiBoChaWebSearchClientTest { + + private final AiBoChaWebSearchClient webSearchClient = new AiBoChaWebSearchClient( + "sk-40500e52840f4d24b956d0b1d80d9abe"); + + @Test + public void testSearch() { + AiWebSearchRequest request = new AiWebSearchRequest() + .setQuery("阿里巴巴") + .setCount(3); + AiWebSearchResponse response = webSearchClient.search(request); + System.out.println(JsonUtils.toJsonPrettyString(response)); + } + +} \ No newline at end of file diff --git a/yudao-server/src/main/resources/application.yaml b/yudao-server/src/main/resources/application.yaml index ad5bc78966..1468dc2019 100644 --- a/yudao-server/src/main/resources/application.yaml +++ b/yudao-server/src/main/resources/application.yaml @@ -234,6 +234,9 @@ yudao: enable: true # base-url: https://suno-55ishh05u-status2xxs-projects.vercel.app base-url: http://127.0.0.1:3001 + web-search: + enable: true + api-key: sk-40500e52840f4d24b956d0b1d80d9abe --- #################### 芋道相关配置 ####################