feat(chat): 实现联网搜索与对话管理功能
- 新增 OkHttp 客户端配置及依赖 - 添加 SearXNG 搜索引擎集成配置 - 创建基础分页查询类 BasePageQuery - 实现网络搜索增强顾问 NetworkSearchAdvisor - 增加聊天历史消息和对话的分页查询接口 - 添加对话摘要重命名与删除功能 - 配置 MyBatis Plus 分页插件支持 - 引入 Jsoup用于网页内容解析 - 新增 Hutool 工具库依赖 - 实现搜索结果内容抓取服务 - 添加搜索结果 DTO 和相关服务接口 - 扩展响应码枚举支持对话不存在情况 - 新增多个 VO 类用于请求和响应数据传输
This commit is contained in:
@@ -0,0 +1,110 @@
|
||||
package com.hanserwei.airobot.service.impl;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.hanserwei.airobot.model.dto.SearchResultDTO;
|
||||
import com.hanserwei.airobot.service.SearXNGService;
|
||||
import jakarta.annotation.Resource;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import okhttp3.HttpUrl;
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.Response;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.StreamSupport;
|
||||
|
||||
/**
|
||||
* SearXNG 搜索服务实现类
|
||||
* <p>
|
||||
* 该类通过调用 SearXNG 的 API 实现聚合搜索引擎功能,支持从多个搜索引擎获取结果并按评分排序。
|
||||
* </p>
|
||||
*/
|
||||
@Service
|
||||
@Slf4j
|
||||
public class SearXNGServiceImpl implements SearXNGService {
|
||||
@Resource
|
||||
private OkHttpClient okHttpClient;
|
||||
@Resource
|
||||
private ObjectMapper objectMapper;
|
||||
@Value("${searxng.url}")
|
||||
private String searxngUrl;
|
||||
@Value("${searxng.count}")
|
||||
private int count;
|
||||
|
||||
/**
|
||||
* 根据关键词执行搜索操作
|
||||
*
|
||||
* @param query 搜索关键词,不能为空
|
||||
* @return 搜索结果列表,每个元素包含 URL 和评分;若发生异常或无结果则返回空列表
|
||||
*/
|
||||
@Override
|
||||
public List<SearchResultDTO> search(String query) {
|
||||
// 构建 SearXNG API 请求 URL
|
||||
HttpUrl httpUrl = Objects.requireNonNull(HttpUrl.parse(searxngUrl)).newBuilder()
|
||||
.addQueryParameter("q", query) // 设置搜索关键词
|
||||
.addQueryParameter("format", "json") // 指定返回 JSON 格式
|
||||
.addQueryParameter("engines", "wolframalpha,presearch,seznam,mwmbl,encyclosearch,bpb,mojeek,right dao,wikimini,crowdview,searchmysite,bing,naver,360search") // 指定聚合的目标搜索引擎(配置本地网络能够访问的通的搜索引擎)
|
||||
.build();
|
||||
|
||||
// 创建 HTTP GET 请求
|
||||
Request request = new Request.Builder()
|
||||
.url(httpUrl)
|
||||
.get()
|
||||
.build();
|
||||
|
||||
// 发送 HTTP 请求
|
||||
try (Response response = okHttpClient.newCall(request).execute()) {
|
||||
// 判断请求是否成功
|
||||
if (response.isSuccessful()) {
|
||||
// 拿到返回结果
|
||||
String result = response.body().string();
|
||||
log.info("## SearXNG 搜索结果: {}", result);
|
||||
|
||||
// 解析 JSON 响应
|
||||
JsonNode root = objectMapper.readTree(result);
|
||||
JsonNode results = root.get("results"); // 获取结果数组节点
|
||||
|
||||
// 定义 Record 类型:用于临时存储分数和节点引用
|
||||
record NodeWithUrlAndScore(double score, JsonNode node) {
|
||||
}
|
||||
|
||||
// 处理搜索结果流:
|
||||
// 1. 提取评分
|
||||
// 2. 按评分降序排序
|
||||
// 3. 限制返回结果数量
|
||||
List<NodeWithUrlAndScore> nodesWithScore = StreamSupport.stream(results.spliterator(), false)
|
||||
.map(node -> {
|
||||
// 只提取分数,避免构建完整对象
|
||||
double score = node.path("score").asDouble(0.0); // 提取评分
|
||||
return new NodeWithUrlAndScore(score, node);
|
||||
})
|
||||
.sorted(Comparator.comparingDouble(NodeWithUrlAndScore::score).reversed()) // 按评分降序
|
||||
.limit(count) // 限制返回结果数量
|
||||
.toList();
|
||||
|
||||
// 转换为 SearchResult 对象集合
|
||||
return nodesWithScore.stream()
|
||||
.map(n -> {
|
||||
JsonNode node = n.node();
|
||||
String originalUrl = node.path("url").asText(""); // 提取 URL
|
||||
return SearchResultDTO.builder()
|
||||
.url(originalUrl)
|
||||
.score(n.score()) // 保留评分
|
||||
.build();
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("", e);
|
||||
}
|
||||
// 返回空集合
|
||||
return Collections.emptyList();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user