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 ) { } } }