feat(chat): 实现联网搜索与对话管理功能

- 新增 OkHttp 客户端配置及依赖
- 添加 SearXNG 搜索引擎集成配置
- 创建基础分页查询类 BasePageQuery
- 实现网络搜索增强顾问 NetworkSearchAdvisor
- 增加聊天历史消息和对话的分页查询接口
- 添加对话摘要重命名与删除功能
- 配置 MyBatis Plus 分页插件支持
- 引入 Jsoup用于网页内容解析
- 新增 Hutool 工具库依赖
- 实现搜索结果内容抓取服务
- 添加搜索结果 DTO 和相关服务接口
- 扩展响应码枚举支持对话不存在情况
- 新增多个 VO 类用于请求和响应数据传输
This commit is contained in:
2025-11-03 22:08:26 +08:00
parent 59eb69747b
commit fdab553ba1
25 changed files with 1076 additions and 7 deletions

View File

@@ -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();
}
}