mirror of
https://github.com/fish2018/pansou.git
synced 2025-11-25 03:14:59 +08:00
1561 lines
45 KiB
Go
1561 lines
45 KiB
Go
package service
|
||
|
||
import (
|
||
"context"
|
||
"fmt"
|
||
"io/ioutil"
|
||
"net/http"
|
||
"net/url"
|
||
"regexp"
|
||
"sort"
|
||
"strings"
|
||
"sync"
|
||
"time"
|
||
|
||
"pansou/config"
|
||
"pansou/model"
|
||
"pansou/plugin"
|
||
"pansou/util"
|
||
"pansou/util/cache"
|
||
"pansou/util/pool"
|
||
)
|
||
|
||
// normalizeUrl 标准化URL,将URL编码的中文部分解码为中文,用于去重
|
||
func normalizeUrl(rawUrl string) string {
|
||
// 解码URL中的编码字符
|
||
decoded, err := url.QueryUnescape(rawUrl)
|
||
if err != nil {
|
||
// 如果解码失败,返回原始URL
|
||
return rawUrl
|
||
}
|
||
return decoded
|
||
}
|
||
|
||
// 全局缓存写入管理器引用(避免循环依赖)
|
||
var globalCacheWriteManager *cache.DelayedBatchWriteManager
|
||
|
||
// SetGlobalCacheWriteManager 设置全局缓存写入管理器
|
||
func SetGlobalCacheWriteManager(manager *cache.DelayedBatchWriteManager) {
|
||
globalCacheWriteManager = manager
|
||
}
|
||
|
||
// GetGlobalCacheWriteManager 获取全局缓存写入管理器
|
||
func GetGlobalCacheWriteManager() *cache.DelayedBatchWriteManager {
|
||
return globalCacheWriteManager
|
||
}
|
||
|
||
// GetEnhancedTwoLevelCache 获取增强版两级缓存实例
|
||
func GetEnhancedTwoLevelCache() *cache.EnhancedTwoLevelCache {
|
||
return enhancedTwoLevelCache
|
||
}
|
||
|
||
// 优先关键词列表
|
||
var priorityKeywords = []string{"合集", "系列", "全", "完", "最新", "附", "complete"}
|
||
|
||
// extractKeywordFromCacheKey 从缓存键中提取关键词(简化版)
|
||
func extractKeywordFromCacheKey(cacheKey string) string {
|
||
// 这是一个简化的实现,实际中我们会通过传递来获得关键词
|
||
// 为了演示,这里返回简化的显示
|
||
return "搜索关键词"
|
||
}
|
||
|
||
// logAsyncCacheWithKeyword 异步缓存日志输出辅助函数(带关键词)
|
||
func logAsyncCacheWithKeyword(keyword, cacheKey string, format string, args ...interface{}) {
|
||
// 检查配置开关
|
||
if config.AppConfig == nil || !config.AppConfig.AsyncLogEnabled {
|
||
return
|
||
}
|
||
|
||
// 构建显示的关键词信息
|
||
displayKeyword := keyword
|
||
if displayKeyword == "" {
|
||
displayKeyword = "未知"
|
||
}
|
||
|
||
// 将缓存键替换为简化版本+关键词
|
||
shortKey := cacheKey
|
||
if len(cacheKey) > 8 {
|
||
shortKey = cacheKey[:8] + "..."
|
||
}
|
||
|
||
// 替换格式字符串中的缓存键
|
||
enhancedFormat := strings.Replace(format, cacheKey, fmt.Sprintf("%s(关键词:%s)", shortKey, displayKeyword), 1)
|
||
fmt.Printf(enhancedFormat, args...)
|
||
}
|
||
|
||
// 全局缓存实例和缓存是否初始化标志
|
||
var (
|
||
enhancedTwoLevelCache *cache.EnhancedTwoLevelCache
|
||
cacheInitialized bool
|
||
)
|
||
|
||
// 初始化缓存
|
||
func init() {
|
||
if config.AppConfig != nil && config.AppConfig.CacheEnabled {
|
||
var err error
|
||
// 使用增强版缓存
|
||
enhancedTwoLevelCache, err = cache.NewEnhancedTwoLevelCache()
|
||
if err == nil {
|
||
cacheInitialized = true
|
||
}
|
||
}
|
||
}
|
||
|
||
// mergeSearchResults 智能合并搜索结果,去重并保留最完整的信息
|
||
func mergeSearchResults(existing []model.SearchResult, newResults []model.SearchResult) []model.SearchResult {
|
||
// 使用map进行去重和合并,以UniqueID作为唯一标识
|
||
resultMap := make(map[string]model.SearchResult)
|
||
|
||
// 先添加现有结果
|
||
for _, result := range existing {
|
||
key := generateResultKey(result)
|
||
resultMap[key] = result
|
||
}
|
||
|
||
// 合并新结果,如果UniqueID相同则选择信息更完整的
|
||
for _, newResult := range newResults {
|
||
key := generateResultKey(newResult)
|
||
if existingResult, exists := resultMap[key]; exists {
|
||
// 选择信息更完整的结果
|
||
resultMap[key] = selectBetterResult(existingResult, newResult)
|
||
} else {
|
||
// 新结果,直接添加
|
||
resultMap[key] = newResult
|
||
}
|
||
}
|
||
|
||
// 转换回切片
|
||
merged := make([]model.SearchResult, 0, len(resultMap))
|
||
for _, result := range resultMap {
|
||
merged = append(merged, result)
|
||
}
|
||
|
||
// 按时间排序(最新的在前)
|
||
sort.Slice(merged, func(i, j int) bool {
|
||
return merged[i].Datetime.After(merged[j].Datetime)
|
||
})
|
||
|
||
return merged
|
||
}
|
||
|
||
// generateResultKey 生成结果的唯一标识键
|
||
func generateResultKey(result model.SearchResult) string {
|
||
// 使用UniqueID作为主要标识,如果没有则使用MessageID,最后使用标题
|
||
if result.UniqueID != "" {
|
||
return result.UniqueID
|
||
}
|
||
if result.MessageID != "" {
|
||
return result.MessageID
|
||
}
|
||
return fmt.Sprintf("title_%s_%s", result.Title, result.Channel)
|
||
}
|
||
|
||
// selectBetterResult 选择信息更完整的结果
|
||
func selectBetterResult(existing, new model.SearchResult) model.SearchResult {
|
||
// 计算信息完整度得分
|
||
existingScore := calculateCompletenessScore(existing)
|
||
newScore := calculateCompletenessScore(new)
|
||
|
||
if newScore > existingScore {
|
||
return new
|
||
}
|
||
return existing
|
||
}
|
||
|
||
// calculateCompletenessScore 计算结果信息的完整度得分
|
||
func calculateCompletenessScore(result model.SearchResult) int {
|
||
score := 0
|
||
|
||
// 有UniqueID加分
|
||
if result.UniqueID != "" {
|
||
score += 10
|
||
}
|
||
|
||
// 有链接信息加分
|
||
if len(result.Links) > 0 {
|
||
score += 5
|
||
// 每个链接额外加分
|
||
score += len(result.Links)
|
||
}
|
||
|
||
// 有内容加分
|
||
if result.Content != "" {
|
||
score += 3
|
||
}
|
||
|
||
// 标题长度加分(更详细的标题)
|
||
score += len(result.Title) / 10
|
||
|
||
// 有频道信息加分
|
||
if result.Channel != "" {
|
||
score += 2
|
||
}
|
||
|
||
// 有标签加分
|
||
score += len(result.Tags)
|
||
|
||
return score
|
||
}
|
||
|
||
// SearchService 搜索服务
|
||
type SearchService struct {
|
||
pluginManager *plugin.PluginManager
|
||
}
|
||
|
||
// NewSearchService 创建搜索服务实例并确保缓存可用
|
||
func NewSearchService(pluginManager *plugin.PluginManager) *SearchService {
|
||
// 检查缓存是否已初始化,如果未初始化则尝试重新初始化
|
||
if !cacheInitialized && config.AppConfig != nil && config.AppConfig.CacheEnabled {
|
||
var err error
|
||
// 使用增强版缓存
|
||
enhancedTwoLevelCache, err = cache.NewEnhancedTwoLevelCache()
|
||
if err == nil {
|
||
cacheInitialized = true
|
||
}
|
||
}
|
||
|
||
// 将主缓存注入到异步插件中
|
||
injectMainCacheToAsyncPlugins(pluginManager, enhancedTwoLevelCache)
|
||
|
||
// 确保缓存写入管理器设置了主缓存更新函数
|
||
if globalCacheWriteManager != nil && enhancedTwoLevelCache != nil {
|
||
globalCacheWriteManager.SetMainCacheUpdater(func(key string, data []byte, ttl time.Duration) error {
|
||
return enhancedTwoLevelCache.SetBothLevels(key, data, ttl)
|
||
})
|
||
}
|
||
|
||
return &SearchService{
|
||
pluginManager: pluginManager,
|
||
}
|
||
}
|
||
|
||
// injectMainCacheToAsyncPlugins 将主缓存系统注入到异步插件中
|
||
func injectMainCacheToAsyncPlugins(pluginManager *plugin.PluginManager, mainCache *cache.EnhancedTwoLevelCache) {
|
||
// 如果缓存或插件管理器不可用,直接返回
|
||
if mainCache == nil || pluginManager == nil {
|
||
return
|
||
}
|
||
|
||
// 设置全局序列化器,确保异步插件与主程序使用相同的序列化格式
|
||
serializer := mainCache.GetSerializer()
|
||
if serializer != nil {
|
||
plugin.SetGlobalCacheSerializer(serializer)
|
||
}
|
||
|
||
// 创建缓存更新函数(支持IsFinal参数)- 接收原始数据并与现有缓存合并
|
||
cacheUpdater := func(key string, newResults []model.SearchResult, ttl time.Duration, isFinal bool, keyword string, pluginName string) error {
|
||
// 优化:如果新结果为空,跳过缓存更新(避免无效操作)
|
||
if len(newResults) == 0 {
|
||
return nil
|
||
}
|
||
|
||
// 获取现有缓存数据进行合并
|
||
var finalResults []model.SearchResult
|
||
if existingData, hit, err := mainCache.Get(key); err == nil && hit {
|
||
var existingResults []model.SearchResult
|
||
if err := mainCache.GetSerializer().Deserialize(existingData, &existingResults); err == nil {
|
||
// 合并新旧结果,去重保留最完整的数据
|
||
finalResults = mergeSearchResults(existingResults, newResults)
|
||
if config.AppConfig != nil && config.AppConfig.AsyncLogEnabled {
|
||
if keyword != "" {
|
||
fmt.Printf("🔄 [%s:%s] 更新缓存| 原有: %d + 新增: %d = 合并后: %d\n",
|
||
pluginName, keyword, len(existingResults), len(newResults), len(finalResults))
|
||
}
|
||
}
|
||
} else {
|
||
// 反序列化失败,使用新结果
|
||
finalResults = newResults
|
||
if config.AppConfig != nil && config.AppConfig.AsyncLogEnabled {
|
||
displayKey := key[:8] + "..."
|
||
if keyword != "" {
|
||
fmt.Printf("[异步插件 %s] 缓存反序列化失败,使用新结果: %s(关键词:%s) | 结果数: %d\n", pluginName, displayKey, keyword, len(newResults))
|
||
} else {
|
||
fmt.Printf("[异步插件 %s] 缓存反序列化失败,使用新结果: %s | 结果数: %d\n", pluginName, key, len(newResults))
|
||
}
|
||
}
|
||
}
|
||
} else {
|
||
// 无现有缓存,直接使用新结果
|
||
finalResults = newResults
|
||
if config.AppConfig != nil && config.AppConfig.AsyncLogEnabled {
|
||
displayKey := key[:8] + "..."
|
||
if keyword != "" {
|
||
fmt.Printf("[异步插件 %s] 初始缓存创建: %s(关键词:%s) | 结果数: %d\n", pluginName, displayKey, keyword, len(newResults))
|
||
} else {
|
||
fmt.Printf("[异步插件 %s] 初始缓存创建: %s | 结果数: %d\n", pluginName, key, len(newResults))
|
||
}
|
||
}
|
||
}
|
||
|
||
// 序列化合并后的结果
|
||
data, err := mainCache.GetSerializer().Serialize(finalResults)
|
||
if err != nil {
|
||
fmt.Printf("[缓存更新] 序列化失败: %s | 错误: %v\n", key, err)
|
||
return err
|
||
}
|
||
|
||
// 先更新内存缓存(立即可见)
|
||
if err := mainCache.SetMemoryOnly(key, data, ttl); err != nil {
|
||
return fmt.Errorf("内存缓存更新失败: %v", err)
|
||
}
|
||
|
||
// 使用新的缓存写入管理器处理磁盘写入(智能批处理)
|
||
if cacheWriteManager := globalCacheWriteManager; cacheWriteManager != nil {
|
||
operation := &cache.CacheOperation{
|
||
Key: key,
|
||
Data: finalResults, // 使用原始数据而不是序列化后的
|
||
TTL: ttl,
|
||
IsFinal: isFinal,
|
||
PluginName: pluginName,
|
||
Keyword: keyword,
|
||
Priority: 2, // 中等优先级
|
||
Timestamp: time.Now(),
|
||
DataSize: len(data), // 序列化后的数据大小
|
||
}
|
||
|
||
// 根据是否为最终结果设置优先级
|
||
if isFinal {
|
||
operation.Priority = 1 // 高优先级
|
||
}
|
||
|
||
return cacheWriteManager.HandleCacheOperation(operation)
|
||
}
|
||
|
||
// 兜底:如果缓存写入管理器不可用,使用原有逻辑
|
||
if isFinal {
|
||
return mainCache.SetBothLevels(key, data, ttl)
|
||
} else {
|
||
return nil // 内存已更新,磁盘稍后批处理
|
||
}
|
||
}
|
||
|
||
// 获取所有插件
|
||
plugins := pluginManager.GetPlugins()
|
||
|
||
// 遍历所有插件,找出异步插件
|
||
for _, p := range plugins {
|
||
// 检查插件是否实现了SetMainCacheUpdater方法(修复后的签名,增加关键词参数)
|
||
if asyncPlugin, ok := p.(interface{ SetMainCacheUpdater(func(string, []model.SearchResult, time.Duration, bool, string) error) }); ok {
|
||
// 为每个插件创建专门的缓存更新函数,绑定插件名称
|
||
pluginName := p.Name()
|
||
pluginCacheUpdater := func(key string, newResults []model.SearchResult, ttl time.Duration, isFinal bool, keyword string) error {
|
||
return cacheUpdater(key, newResults, ttl, isFinal, keyword, pluginName)
|
||
}
|
||
// 注入缓存更新函数
|
||
asyncPlugin.SetMainCacheUpdater(pluginCacheUpdater)
|
||
}
|
||
}
|
||
}
|
||
|
||
// Search 执行搜索
|
||
func (s *SearchService) Search(keyword string, channels []string, concurrency int, forceRefresh bool, resultType string, sourceType string, plugins []string, cloudTypes []string, ext map[string]interface{}) (model.SearchResponse, error) {
|
||
// 确保ext不为nil
|
||
if ext == nil {
|
||
ext = make(map[string]interface{})
|
||
}
|
||
|
||
// 参数预处理
|
||
// 源类型标准化
|
||
if sourceType == "" {
|
||
sourceType = "all"
|
||
}
|
||
|
||
// 插件参数规范化处理
|
||
if sourceType == "tg" {
|
||
// 对于只搜索Telegram的请求,忽略插件参数
|
||
plugins = nil
|
||
} else if sourceType == "all" || sourceType == "plugin" {
|
||
// 检查是否为空列表或只包含空字符串
|
||
if plugins == nil || len(plugins) == 0 {
|
||
plugins = nil
|
||
} else {
|
||
// 检查是否有非空元素
|
||
hasNonEmpty := false
|
||
for _, p := range plugins {
|
||
if p != "" {
|
||
hasNonEmpty = true
|
||
break
|
||
}
|
||
}
|
||
|
||
// 如果全是空字符串,视为未指定
|
||
if !hasNonEmpty {
|
||
plugins = nil
|
||
} else {
|
||
// 检查是否包含所有插件
|
||
allPlugins := s.pluginManager.GetPlugins()
|
||
allPluginNames := make([]string, 0, len(allPlugins))
|
||
for _, p := range allPlugins {
|
||
allPluginNames = append(allPluginNames, strings.ToLower(p.Name()))
|
||
}
|
||
|
||
// 创建请求的插件名称集合(忽略空字符串)
|
||
requestedPlugins := make([]string, 0, len(plugins))
|
||
for _, p := range plugins {
|
||
if p != "" {
|
||
requestedPlugins = append(requestedPlugins, strings.ToLower(p))
|
||
}
|
||
}
|
||
|
||
// 如果请求的插件数量与所有插件数量相同,检查是否包含所有插件
|
||
if len(requestedPlugins) == len(allPluginNames) {
|
||
// 创建映射以便快速查找
|
||
pluginMap := make(map[string]bool)
|
||
for _, p := range requestedPlugins {
|
||
pluginMap[p] = true
|
||
}
|
||
|
||
// 检查是否包含所有插件
|
||
allIncluded := true
|
||
for _, name := range allPluginNames {
|
||
if !pluginMap[name] {
|
||
allIncluded = false
|
||
break
|
||
}
|
||
}
|
||
|
||
// 如果包含所有插件,统一设为nil
|
||
if allIncluded {
|
||
plugins = nil
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 如果未指定并发数,使用配置中的默认值
|
||
if concurrency <= 0 {
|
||
concurrency = config.AppConfig.DefaultConcurrency
|
||
}
|
||
|
||
// 并行获取TG搜索和插件搜索结果
|
||
var tgResults []model.SearchResult
|
||
var pluginResults []model.SearchResult
|
||
|
||
var wg sync.WaitGroup
|
||
var tgErr, pluginErr error
|
||
|
||
// 如果需要搜索TG
|
||
if sourceType == "all" || sourceType == "tg" {
|
||
wg.Add(1)
|
||
go func() {
|
||
defer wg.Done()
|
||
tgResults, tgErr = s.searchTG(keyword, channels, forceRefresh)
|
||
}()
|
||
}
|
||
// 如果需要搜索插件(且插件功能已启用)
|
||
if (sourceType == "all" || sourceType == "plugin") && config.AppConfig.AsyncPluginEnabled {
|
||
wg.Add(1)
|
||
go func() {
|
||
defer wg.Done()
|
||
// 对于插件搜索,我们总是希望获取最新的缓存数据
|
||
// 因此,即使forceRefresh=false,我们也需要确保获取到最新的缓存
|
||
pluginResults, pluginErr = s.searchPlugins(keyword, plugins, forceRefresh, concurrency, ext)
|
||
}()
|
||
}
|
||
|
||
// 等待所有搜索完成
|
||
wg.Wait()
|
||
|
||
// 检查错误
|
||
if tgErr != nil {
|
||
return model.SearchResponse{}, tgErr
|
||
}
|
||
if pluginErr != nil {
|
||
return model.SearchResponse{}, pluginErr
|
||
}
|
||
|
||
// 合并结果
|
||
allResults := mergeSearchResults(tgResults, pluginResults)
|
||
|
||
// 按照优化后的规则排序结果
|
||
sortResultsByTimeAndKeywords(allResults)
|
||
|
||
// 过滤结果,只保留有时间的结果或包含优先关键词的结果或高等级插件结果到Results中
|
||
filteredForResults := make([]model.SearchResult, 0, len(allResults))
|
||
for _, result := range allResults {
|
||
source := getResultSource(result)
|
||
pluginLevel := getPluginLevelBySource(source)
|
||
|
||
// 有时间的结果或包含优先关键词的结果或高等级插件(1-2级)结果保留在Results中
|
||
if !result.Datetime.IsZero() || getKeywordPriority(result.Title) > 0 || pluginLevel <= 2 {
|
||
filteredForResults = append(filteredForResults, result)
|
||
}
|
||
}
|
||
|
||
// 合并链接按网盘类型分组(使用所有过滤后的结果)
|
||
mergedLinks := mergeResultsByType(allResults, keyword, cloudTypes)
|
||
|
||
// 构建响应
|
||
var total int
|
||
if resultType == "merged_by_type" {
|
||
// 计算所有类型链接的总数
|
||
total = 0
|
||
for _, links := range mergedLinks {
|
||
total += len(links)
|
||
}
|
||
} else {
|
||
// 只计算filteredForResults的数量
|
||
total = len(filteredForResults)
|
||
}
|
||
|
||
response := model.SearchResponse{
|
||
Total: total,
|
||
Results: filteredForResults, // 使用进一步过滤的结果
|
||
MergedByType: mergedLinks,
|
||
}
|
||
|
||
// 根据resultType过滤返回结果
|
||
return filterResponseByType(response, resultType), nil
|
||
}
|
||
|
||
// filterResponseByType 根据结果类型过滤响应
|
||
func filterResponseByType(response model.SearchResponse, resultType string) model.SearchResponse {
|
||
switch resultType {
|
||
case "merged_by_type":
|
||
// 只返回MergedByType,Results设为nil,结合omitempty标签,JSON序列化时会忽略此字段
|
||
return model.SearchResponse{
|
||
Total: response.Total,
|
||
MergedByType: response.MergedByType,
|
||
Results: nil,
|
||
}
|
||
case "all":
|
||
return response
|
||
case "results":
|
||
// 只返回Results
|
||
return model.SearchResponse{
|
||
Total: response.Total,
|
||
Results: response.Results,
|
||
}
|
||
default:
|
||
// // 默认返回全部
|
||
// return response
|
||
return model.SearchResponse{
|
||
Total: response.Total,
|
||
MergedByType: response.MergedByType,
|
||
Results: nil,
|
||
}
|
||
}
|
||
}
|
||
|
||
// 根据时间和关键词排序结果
|
||
func sortResultsByTimeAndKeywords(results []model.SearchResult) {
|
||
// 1. 计算每个结果的综合得分
|
||
scores := make([]ResultScore, len(results))
|
||
|
||
for i, result := range results {
|
||
source := getResultSource(result)
|
||
|
||
scores[i] = ResultScore{
|
||
Result: result,
|
||
TimeScore: calculateTimeScore(result.Datetime),
|
||
KeywordScore: getKeywordPriority(result.Title),
|
||
PluginScore: getPluginLevelScore(source),
|
||
TotalScore: 0, // 稍后计算
|
||
}
|
||
|
||
// 计算综合得分
|
||
scores[i].TotalScore = scores[i].TimeScore +
|
||
float64(scores[i].KeywordScore) +
|
||
float64(scores[i].PluginScore)
|
||
}
|
||
|
||
// 2. 按综合得分排序
|
||
sort.Slice(scores, func(i, j int) bool {
|
||
return scores[i].TotalScore > scores[j].TotalScore
|
||
})
|
||
|
||
// 3. 更新原数组
|
||
for i, score := range scores {
|
||
results[i] = score.Result
|
||
}
|
||
}
|
||
|
||
|
||
|
||
|
||
|
||
// 获取标题中包含优先关键词的优先级
|
||
func getKeywordPriority(title string) int {
|
||
title = strings.ToLower(title)
|
||
for i, keyword := range priorityKeywords {
|
||
if strings.Contains(title, keyword) {
|
||
// 返回优先级得分(数组索引越小,优先级越高,最高400分)
|
||
return (len(priorityKeywords) - i) * 70
|
||
}
|
||
}
|
||
return 0
|
||
}
|
||
|
||
// 搜索单个频道
|
||
func (s *SearchService) searchChannel(keyword string, channel string) ([]model.SearchResult, error) {
|
||
// 构建搜索URL
|
||
url := util.BuildSearchURL(channel, keyword, "")
|
||
|
||
// 使用全局HTTP客户端(已配置代理)
|
||
client := util.GetHTTPClient()
|
||
|
||
// 创建一个带超时的上下文
|
||
ctx, cancel := context.WithTimeout(context.Background(), 4*time.Second)
|
||
defer cancel()
|
||
|
||
// 创建请求
|
||
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
// 发送请求
|
||
resp, err := client.Do(req)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
defer resp.Body.Close()
|
||
|
||
// 读取响应体
|
||
body, err := ioutil.ReadAll(resp.Body)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
// 解析响应
|
||
results, _, err := util.ParseSearchResults(string(body), channel)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
return results, nil
|
||
}
|
||
|
||
// 用于从消息内容中提取链接-标题对应关系的函数
|
||
func extractLinkTitlePairs(content string) map[string]string {
|
||
// 首先尝试使用换行符分割的方法
|
||
if strings.Contains(content, "\n") {
|
||
return extractLinkTitlePairsWithNewlines(content)
|
||
}
|
||
|
||
// 如果没有换行符,使用正则表达式直接提取
|
||
return extractLinkTitlePairsWithoutNewlines(content)
|
||
}
|
||
|
||
// 处理有换行符的情况
|
||
func extractLinkTitlePairsWithNewlines(content string) map[string]string {
|
||
// 结果映射:链接URL -> 对应标题
|
||
linkTitleMap := make(map[string]string)
|
||
|
||
// 按行分割内容
|
||
lines := strings.Split(content, "\n")
|
||
|
||
// 链接正则表达式
|
||
linkRegex := regexp.MustCompile(`https?://[^\s"']+`)
|
||
|
||
// 第一遍扫描:识别标题-链接对
|
||
var lastTitle string
|
||
var lastTitleIndex int
|
||
|
||
for i := 0; i < len(lines); i++ {
|
||
line := strings.TrimSpace(lines[i])
|
||
if line == "" {
|
||
continue
|
||
}
|
||
|
||
// 检查当前行是否包含链接
|
||
links := linkRegex.FindAllString(line, -1)
|
||
|
||
if len(links) > 0 {
|
||
// 当前行包含链接
|
||
|
||
// 检查是否是标准链接行(以"链接:"、"地址:"等开头)
|
||
isStandardLinkLine := isLinkLine(line)
|
||
|
||
if isStandardLinkLine && lastTitle != "" {
|
||
// 标准链接行,使用上一个标题
|
||
for _, link := range links {
|
||
linkTitleMap[link] = lastTitle
|
||
}
|
||
} else if !isStandardLinkLine {
|
||
// 非标准链接行,可能是"标题:链接"格式
|
||
titleFromLine := extractTitleFromLinkLine(line)
|
||
if titleFromLine != "" {
|
||
// 是"标题:链接"格式
|
||
for _, link := range links {
|
||
linkTitleMap[link] = titleFromLine
|
||
}
|
||
} else if lastTitle != "" {
|
||
// 其他情况,使用上一个标题
|
||
for _, link := range links {
|
||
linkTitleMap[link] = lastTitle
|
||
}
|
||
}
|
||
}
|
||
} else {
|
||
// 当前行不包含链接,可能是标题行
|
||
// 检查下一行是否为链接行
|
||
if i+1 < len(lines) {
|
||
nextLine := strings.TrimSpace(lines[i+1])
|
||
if isLinkLine(nextLine) || linkRegex.MatchString(nextLine) {
|
||
// 下一行是链接行或包含链接,当前行很可能是标题
|
||
lastTitle = cleanTitle(line)
|
||
lastTitleIndex = i
|
||
}
|
||
} else {
|
||
// 最后一行,也可能是标题
|
||
lastTitle = cleanTitle(line)
|
||
lastTitleIndex = i
|
||
}
|
||
}
|
||
}
|
||
|
||
// 第二遍扫描:处理没有匹配到标题的链接
|
||
// 为每个链接找到最近的上文标题
|
||
for i := 0; i < len(lines); i++ {
|
||
line := strings.TrimSpace(lines[i])
|
||
if line == "" {
|
||
continue
|
||
}
|
||
|
||
links := linkRegex.FindAllString(line, -1)
|
||
if len(links) == 0 {
|
||
continue
|
||
}
|
||
|
||
for _, link := range links {
|
||
if _, exists := linkTitleMap[link]; !exists {
|
||
// 链接没有匹配到标题,尝试找最近的上文标题
|
||
nearestTitle := ""
|
||
|
||
// 向上查找最近的标题行
|
||
for j := i - 1; j >= 0; j-- {
|
||
if j == lastTitleIndex || (j+1 < len(lines) &&
|
||
linkRegex.MatchString(lines[j+1]) &&
|
||
!linkRegex.MatchString(lines[j])) {
|
||
candidateTitle := cleanTitle(lines[j])
|
||
if candidateTitle != "" {
|
||
nearestTitle = candidateTitle
|
||
break
|
||
}
|
||
}
|
||
}
|
||
|
||
if nearestTitle != "" {
|
||
linkTitleMap[link] = nearestTitle
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
return linkTitleMap
|
||
}
|
||
|
||
// 处理没有换行符的情况
|
||
func extractLinkTitlePairsWithoutNewlines(content string) map[string]string {
|
||
// 结果映射:链接URL -> 对应标题
|
||
linkTitleMap := make(map[string]string)
|
||
|
||
// 使用精确的网盘链接正则表达式集合,避免贪婪匹配
|
||
linkPatterns := []*regexp.Regexp{
|
||
util.TianyiPanPattern, // 天翼云盘
|
||
util.BaiduPanPattern, // 百度网盘
|
||
util.QuarkPanPattern, // 夸克网盘
|
||
util.AliyunPanPattern, // 阿里云盘
|
||
util.UCPanPattern, // UC网盘
|
||
util.Pan123Pattern, // 123网盘
|
||
util.Pan115Pattern, // 115网盘
|
||
util.XunleiPanPattern, // 迅雷网盘
|
||
}
|
||
|
||
// 收集所有链接及其位置
|
||
type linkInfo struct {
|
||
url string
|
||
pos int
|
||
}
|
||
var allLinks []linkInfo
|
||
|
||
// 使用各个精确正则表达式查找链接
|
||
for _, pattern := range linkPatterns {
|
||
matches := pattern.FindAllString(content, -1)
|
||
for _, match := range matches {
|
||
pos := strings.Index(content, match)
|
||
if pos >= 0 {
|
||
allLinks = append(allLinks, linkInfo{url: match, pos: pos})
|
||
}
|
||
}
|
||
}
|
||
|
||
// 按位置排序
|
||
for i := 0; i < len(allLinks)-1; i++ {
|
||
for j := i + 1; j < len(allLinks); j++ {
|
||
if allLinks[i].pos > allLinks[j].pos {
|
||
allLinks[i], allLinks[j] = allLinks[j], allLinks[i]
|
||
}
|
||
}
|
||
}
|
||
|
||
// URL标准化和去重
|
||
uniqueLinks := make(map[string]string) // 标准化URL -> 原始URL
|
||
var links []string
|
||
|
||
for _, linkInfo := range allLinks {
|
||
// 标准化URL(将URL编码转换为中文)
|
||
normalized := normalizeUrl(linkInfo.url)
|
||
|
||
// 如果这个标准化URL还没有见过,则保留
|
||
if _, exists := uniqueLinks[normalized]; !exists {
|
||
uniqueLinks[normalized] = linkInfo.url
|
||
links = append(links, linkInfo.url)
|
||
}
|
||
}
|
||
|
||
if len(links) == 0 {
|
||
return linkTitleMap
|
||
}
|
||
|
||
// 使用链接位置分割内容
|
||
segments := make([]string, len(links)+1)
|
||
lastPos := 0
|
||
|
||
// 查找每个链接的位置,并提取链接前的文本作为段落
|
||
for i, link := range links {
|
||
idx := strings.Index(content[lastPos:], link)
|
||
if idx == -1 {
|
||
// 链接在content中不存在,跳过
|
||
continue
|
||
}
|
||
pos := idx + lastPos
|
||
if pos > lastPos {
|
||
segments[i] = content[lastPos:pos]
|
||
}
|
||
lastPos = pos + len(link)
|
||
}
|
||
|
||
// 最后一段
|
||
if lastPos < len(content) {
|
||
segments[len(links)] = content[lastPos:]
|
||
}
|
||
|
||
// 从每个段落中提取标题
|
||
for i, link := range links {
|
||
// 当前链接的标题应该在当前段落的末尾
|
||
var title string
|
||
|
||
// 如果是第一个链接
|
||
if i == 0 {
|
||
// 提取第一个段落作为标题
|
||
title = extractTitleBeforeLink(segments[i])
|
||
} else {
|
||
// 从上一个链接后的文本中提取标题
|
||
title = extractTitleBeforeLink(segments[i])
|
||
}
|
||
|
||
// 如果提取到了标题,保存链接-标题对应关系
|
||
if title != "" {
|
||
linkTitleMap[link] = title
|
||
}
|
||
}
|
||
|
||
return linkTitleMap
|
||
}
|
||
|
||
// 从文本中提取链接前的标题
|
||
func extractTitleBeforeLink(text string) string {
|
||
// 移除可能的链接前缀词
|
||
text = strings.TrimSpace(text)
|
||
|
||
// 查找"链接:"前的文本作为标题
|
||
if idx := strings.Index(text, "链接:"); idx > 0 {
|
||
return cleanTitle(text[:idx])
|
||
}
|
||
|
||
// 尝试匹配常见的标题模式
|
||
titlePattern := regexp.MustCompile(`([^链地资网\s]+?(?:\([^)]+\))?(?:\s*\d+K)?(?:\s*臻彩)?(?:\s*MAX)?(?:\s*HDR)?(?:\s*更(?:新)?\d+集))$`)
|
||
matches := titlePattern.FindStringSubmatch(text)
|
||
if len(matches) > 1 {
|
||
return cleanTitle(matches[1])
|
||
}
|
||
|
||
return cleanTitle(text)
|
||
}
|
||
|
||
// 判断一行是否为链接行(主要包含链接的行)
|
||
func isLinkLine(line string) bool {
|
||
lowerLine := strings.ToLower(line)
|
||
return strings.HasPrefix(lowerLine, "链接:") ||
|
||
strings.HasPrefix(lowerLine, "地址:") ||
|
||
strings.HasPrefix(lowerLine, "资源地址:") ||
|
||
strings.HasPrefix(lowerLine, "网盘:") ||
|
||
strings.HasPrefix(lowerLine, "网盘地址:") ||
|
||
strings.HasPrefix(lowerLine, "链接:")
|
||
}
|
||
|
||
// 从链接行中提取可能的标题
|
||
func extractTitleFromLinkLine(line string) string {
|
||
// 处理"标题:链接"格式
|
||
parts := strings.SplitN(line, ":", 2)
|
||
if len(parts) == 2 && !strings.Contains(parts[0], "http") &&
|
||
!isLinkPrefix(parts[0]) {
|
||
return cleanTitle(parts[0])
|
||
}
|
||
|
||
// 处理"标题:链接"格式(半角冒号)
|
||
parts = strings.SplitN(line, ":", 2)
|
||
if len(parts) == 2 && !strings.Contains(parts[0], "http") &&
|
||
!isLinkPrefix(parts[0]) {
|
||
return cleanTitle(parts[0])
|
||
}
|
||
|
||
return ""
|
||
}
|
||
|
||
// 判断是否为链接前缀词(包括网盘名称)
|
||
func isLinkPrefix(text string) bool {
|
||
text = strings.ToLower(strings.TrimSpace(text))
|
||
|
||
// 标准链接前缀词
|
||
if text == "链接" ||
|
||
text == "地址" ||
|
||
text == "资源地址" ||
|
||
text == "网盘" ||
|
||
text == "网盘地址" {
|
||
return true
|
||
}
|
||
|
||
// 网盘名称(防止误将网盘名称当作标题)
|
||
cloudDiskNames := []string{
|
||
// 夸克网盘
|
||
"夸克", "夸克网盘", "quark", "夸克云盘",
|
||
|
||
// 百度网盘
|
||
"百度", "百度网盘", "baidu", "百度云", "bdwp", "bdpan",
|
||
|
||
// 迅雷网盘
|
||
"迅雷", "迅雷网盘", "xunlei", "迅雷云盘",
|
||
|
||
// 115网盘
|
||
"115", "115网盘", "115云盘",
|
||
|
||
// 123网盘
|
||
"123", "123pan", "123网盘", "123云盘",
|
||
|
||
// 阿里云盘
|
||
"阿里", "阿里云", "阿里云盘", "aliyun", "alipan", "阿里网盘",
|
||
|
||
// 天翼云盘
|
||
"天翼", "天翼云", "天翼云盘", "tianyi", "天翼网盘",
|
||
|
||
// UC网盘
|
||
"uc", "uc网盘", "uc云盘",
|
||
|
||
// 移动云盘
|
||
"移动", "移动云", "移动云盘", "caiyun", "彩云",
|
||
|
||
// PikPak
|
||
"pikpak", "pikpak网盘",
|
||
}
|
||
|
||
for _, name := range cloudDiskNames {
|
||
if text == name {
|
||
return true
|
||
}
|
||
}
|
||
|
||
return false
|
||
}
|
||
|
||
// 清理标题文本
|
||
func cleanTitle(title string) string {
|
||
// 移除常见的无关前缀
|
||
title = strings.TrimSpace(title)
|
||
title = strings.TrimPrefix(title, "名称:")
|
||
title = strings.TrimPrefix(title, "标题:")
|
||
title = strings.TrimPrefix(title, "片名:")
|
||
title = strings.TrimPrefix(title, "名称:")
|
||
title = strings.TrimPrefix(title, "标题:")
|
||
title = strings.TrimPrefix(title, "片名:")
|
||
|
||
// 移除表情符号和特殊字符
|
||
emojiRegex := regexp.MustCompile(`[\p{So}\p{Sk}]`)
|
||
title = emojiRegex.ReplaceAllString(title, "")
|
||
|
||
return strings.TrimSpace(title)
|
||
}
|
||
|
||
// 判断一行是否为空或只包含空白字符
|
||
func isEmpty(line string) bool {
|
||
return strings.TrimSpace(line) == ""
|
||
}
|
||
|
||
// 将搜索结果按网盘类型分组
|
||
func mergeResultsByType(results []model.SearchResult, keyword string, cloudTypes []string) model.MergedLinks {
|
||
// 创建合并结果的映射
|
||
mergedLinks := make(model.MergedLinks, 12) // 预分配容量,假设有12种不同的网盘类型
|
||
|
||
// 用于去重的映射,键为URL
|
||
uniqueLinks := make(map[string]model.MergedLink)
|
||
|
||
// 将关键词转为小写,用于不区分大小写的匹配
|
||
lowerKeyword := strings.ToLower(keyword)
|
||
|
||
// 遍历所有搜索结果
|
||
for _, result := range results {
|
||
// 提取消息中的链接-标题对应关系
|
||
linkTitleMap := extractLinkTitlePairs(result.Content)
|
||
|
||
// 如果没有从内容中提取到标题,尝试直接从内容中匹配
|
||
if len(linkTitleMap) == 0 && len(result.Links) > 0 && !strings.Contains(result.Content, "\n") {
|
||
// 这是没有换行符的情况,尝试直接匹配
|
||
content := result.Content
|
||
|
||
// 支持多种网盘链接前缀
|
||
linkPrefixes := []string{"天翼链接:", "百度链接:", "夸克链接:", "阿里链接:", "UC链接:", "115链接:", "迅雷链接:", "123链接:", "链接:"}
|
||
|
||
var parts []string
|
||
|
||
// 尝试找到匹配的前缀
|
||
for _, prefix := range linkPrefixes {
|
||
if strings.Contains(content, prefix) {
|
||
parts = strings.Split(content, prefix)
|
||
break
|
||
}
|
||
}
|
||
|
||
// 如果找到了匹配的前缀并且分割成功
|
||
if len(parts) > 1 && len(result.Links) <= len(parts)-1 {
|
||
// 第一部分是第一个标题
|
||
titles := make([]string, 0, len(parts))
|
||
titles = append(titles, cleanTitle(parts[0]))
|
||
|
||
// 处理每个包含链接的部分,提取标题
|
||
for i := 1; i < len(parts)-1; i++ {
|
||
part := parts[i]
|
||
// 找到链接的结束位置,使用更通用的分隔符
|
||
linkEnd := -1
|
||
for j, c := range part {
|
||
// 扩展分隔符列表,包含更多可能的字符
|
||
if c == ' ' || c == '窃' || c == '东' || c == '迎' || c == '千' || c == '我' || c == '恋' || c == '将' || c == '野' ||
|
||
c == '合' || c == '集' || c == '天' || c == '翼' || c == '网' || c == '盘' || c == '(' || c == '(' {
|
||
linkEnd = j
|
||
break
|
||
}
|
||
}
|
||
|
||
if linkEnd > 0 {
|
||
// 提取标题
|
||
title := cleanTitle(part[linkEnd:])
|
||
titles = append(titles, title)
|
||
}
|
||
}
|
||
|
||
// 将标题与链接关联
|
||
for i, link := range result.Links {
|
||
if i < len(titles) {
|
||
linkTitleMap[link.URL] = titles[i]
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
for _, link := range result.Links {
|
||
// 优先使用链接的WorkTitle字段,如果为空则回退到传统方式
|
||
title := result.Title // 默认使用消息标题
|
||
|
||
if link.WorkTitle != "" {
|
||
// 如果链接有WorkTitle字段,优先使用
|
||
title = link.WorkTitle
|
||
} else {
|
||
// 如果没有WorkTitle,使用传统方式从映射中获取该链接对应的标题
|
||
// 查找完全匹配的链接
|
||
if specificTitle, found := linkTitleMap[link.URL]; found && specificTitle != "" {
|
||
title = specificTitle // 如果找到特定标题,则使用它
|
||
} else {
|
||
// 如果没有找到完全匹配的链接,尝试查找前缀匹配的链接
|
||
for mappedLink, mappedTitle := range linkTitleMap {
|
||
if strings.HasPrefix(mappedLink, link.URL) {
|
||
title = mappedTitle
|
||
break
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 检查插件是否需要跳过Service层过滤
|
||
var skipKeywordFilter bool = false
|
||
if result.UniqueID != "" && strings.Contains(result.UniqueID, "-") {
|
||
parts := strings.SplitN(result.UniqueID, "-", 2)
|
||
if len(parts) >= 1 {
|
||
pluginName := parts[0]
|
||
// 通过插件注册表动态获取过滤设置
|
||
if pluginInstance, exists := plugin.GetPluginByName(pluginName); exists {
|
||
skipKeywordFilter = pluginInstance.SkipServiceFilter()
|
||
}
|
||
}
|
||
}
|
||
|
||
// 关键词过滤:现在我们有了准确的链接-标题对应关系,只需检查每个链接的具体标题
|
||
if !skipKeywordFilter && keyword != "" {
|
||
// 只检查链接的具体标题,无论是TG来源还是插件来源
|
||
if !strings.Contains(strings.ToLower(title), lowerKeyword) {
|
||
continue
|
||
}
|
||
}
|
||
|
||
// 确定数据来源
|
||
var source string
|
||
if result.Channel != "" {
|
||
// 来自TG频道
|
||
source = "tg:" + result.Channel
|
||
} else if result.UniqueID != "" && strings.Contains(result.UniqueID, "-") {
|
||
// 来自插件:UniqueID格式通常为 "插件名-ID"
|
||
parts := strings.SplitN(result.UniqueID, "-", 2)
|
||
if len(parts) >= 1 {
|
||
source = "plugin:" + parts[0]
|
||
}
|
||
} else {
|
||
// 无法确定来源,使用默认值
|
||
source = "unknown"
|
||
}
|
||
|
||
// 赋值给Note前,支持多个关键词裁剪
|
||
title = util.CutTitleByKeywords(title, []string{"简介", "描述"})
|
||
|
||
// 优先使用链接自己的时间,如果没有则使用搜索结果的时间
|
||
linkDatetime := result.Datetime
|
||
if !link.Datetime.IsZero() {
|
||
linkDatetime = link.Datetime
|
||
}
|
||
|
||
mergedLink := model.MergedLink{
|
||
URL: link.URL,
|
||
Password: link.Password,
|
||
Note: title, // 使用找到的特定标题
|
||
Datetime: linkDatetime,
|
||
Source: source, // 添加数据来源字段
|
||
Images: result.Images, // 添加TG消息中的图片链接
|
||
}
|
||
|
||
// 检查是否已存在相同URL的链接
|
||
if existingLink, exists := uniqueLinks[link.URL]; exists {
|
||
// 如果已存在,只有当当前链接的时间更新时才替换
|
||
if mergedLink.Datetime.After(existingLink.Datetime) {
|
||
uniqueLinks[link.URL] = mergedLink
|
||
}
|
||
} else {
|
||
// 如果不存在,直接添加
|
||
uniqueLinks[link.URL] = mergedLink
|
||
}
|
||
}
|
||
}
|
||
|
||
// 为保持排序顺序,按原始results顺序处理链接,而不是随机遍历map
|
||
// 创建一个有序的链接列表,按原始results中的顺序
|
||
orderedLinks := make([]model.MergedLink, 0, len(uniqueLinks))
|
||
linkTypeMap := make(map[string]string) // URL -> Type的映射
|
||
|
||
// 按原始results的顺序收集唯一链接
|
||
for _, result := range results {
|
||
for _, link := range result.Links {
|
||
if mergedLink, exists := uniqueLinks[link.URL]; exists {
|
||
// 检查是否已经添加过这个链接
|
||
found := false
|
||
for _, existing := range orderedLinks {
|
||
if existing.URL == link.URL {
|
||
found = true
|
||
break
|
||
}
|
||
}
|
||
if !found {
|
||
orderedLinks = append(orderedLinks, mergedLink)
|
||
linkTypeMap[link.URL] = link.Type
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 将有序链接按类型分组
|
||
for _, mergedLink := range orderedLinks {
|
||
// 从预建的映射中获取链接类型
|
||
linkType := linkTypeMap[mergedLink.URL]
|
||
if linkType == "" {
|
||
linkType = "unknown"
|
||
}
|
||
|
||
// 添加到对应类型的列表中
|
||
mergedLinks[linkType] = append(mergedLinks[linkType], mergedLink)
|
||
}
|
||
|
||
|
||
// 如果指定了cloudTypes,则过滤结果
|
||
if len(cloudTypes) > 0 {
|
||
// 创建过滤后的结果映射
|
||
filteredLinks := make(model.MergedLinks)
|
||
|
||
// 将cloudTypes转换为map以提高查找性能
|
||
allowedTypes := make(map[string]bool)
|
||
for _, cloudType := range cloudTypes {
|
||
allowedTypes[strings.ToLower(strings.TrimSpace(cloudType))] = true
|
||
}
|
||
|
||
// 只保留指定类型的链接
|
||
for linkType, links := range mergedLinks {
|
||
if allowedTypes[strings.ToLower(linkType)] {
|
||
filteredLinks[linkType] = links
|
||
}
|
||
}
|
||
|
||
return filteredLinks
|
||
}
|
||
|
||
return mergedLinks
|
||
}
|
||
|
||
// searchTG 搜索TG频道
|
||
func (s *SearchService) searchTG(keyword string, channels []string, forceRefresh bool) ([]model.SearchResult, error) {
|
||
// 生成缓存键
|
||
cacheKey := cache.GenerateTGCacheKey(keyword, channels)
|
||
|
||
// 如果未启用强制刷新,尝试从缓存获取结果
|
||
if !forceRefresh && cacheInitialized && config.AppConfig.CacheEnabled {
|
||
var data []byte
|
||
var hit bool
|
||
var err error
|
||
|
||
// 使用增强版缓存
|
||
if enhancedTwoLevelCache != nil {
|
||
data, hit, err = enhancedTwoLevelCache.Get(cacheKey)
|
||
|
||
if err == nil && hit {
|
||
var results []model.SearchResult
|
||
if err := enhancedTwoLevelCache.GetSerializer().Deserialize(data, &results); err == nil {
|
||
// 直接返回缓存数据,不检查新鲜度
|
||
return results, nil
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 缓存未命中或强制刷新,执行实际搜索
|
||
var results []model.SearchResult
|
||
|
||
// 使用工作池并行搜索多个频道
|
||
tasks := make([]pool.Task, 0, len(channels))
|
||
|
||
for _, channel := range channels {
|
||
ch := channel // 创建副本,避免闭包问题
|
||
tasks = append(tasks, func() interface{} {
|
||
results, err := s.searchChannel(keyword, ch)
|
||
if err != nil {
|
||
return nil
|
||
}
|
||
return results
|
||
})
|
||
}
|
||
|
||
// 执行搜索任务并获取结果
|
||
taskResults := pool.ExecuteBatchWithTimeout(tasks, len(channels), config.AppConfig.PluginTimeout)
|
||
|
||
// 合并所有频道的结果
|
||
for _, result := range taskResults {
|
||
if result != nil {
|
||
channelResults := result.([]model.SearchResult)
|
||
results = append(results, channelResults...)
|
||
}
|
||
}
|
||
|
||
// 异步缓存结果
|
||
if cacheInitialized && config.AppConfig.CacheEnabled {
|
||
go func(res []model.SearchResult) {
|
||
ttl := time.Duration(config.AppConfig.CacheTTLMinutes) * time.Minute
|
||
|
||
// 使用增强版缓存
|
||
if enhancedTwoLevelCache != nil {
|
||
data, err := enhancedTwoLevelCache.GetSerializer().Serialize(res)
|
||
if err != nil {
|
||
return
|
||
}
|
||
enhancedTwoLevelCache.Set(cacheKey, data, ttl)
|
||
}
|
||
}(results)
|
||
}
|
||
|
||
return results, nil
|
||
}
|
||
|
||
// searchPlugins 搜索插件
|
||
func (s *SearchService) searchPlugins(keyword string, plugins []string, forceRefresh bool, concurrency int, ext map[string]interface{}) ([]model.SearchResult, error) {
|
||
// 确保ext不为nil
|
||
if ext == nil {
|
||
ext = make(map[string]interface{})
|
||
}
|
||
|
||
// 关键:将forceRefresh同步到插件ext["refresh"]
|
||
if forceRefresh {
|
||
ext["refresh"] = true
|
||
}
|
||
|
||
// 生成缓存键
|
||
cacheKey := cache.GeneratePluginCacheKey(keyword, plugins)
|
||
|
||
|
||
// 如果未启用强制刷新,尝试从缓存获取结果
|
||
if !forceRefresh && cacheInitialized && config.AppConfig.CacheEnabled {
|
||
var data []byte
|
||
var hit bool
|
||
var err error
|
||
|
||
// 使用增强版缓存
|
||
if enhancedTwoLevelCache != nil {
|
||
|
||
// 使用Get方法,它会检查磁盘缓存是否有更新
|
||
// 如果磁盘缓存比内存缓存更新,会自动更新内存缓存并返回最新数据
|
||
data, hit, err = enhancedTwoLevelCache.Get(cacheKey)
|
||
|
||
if err == nil && hit {
|
||
var results []model.SearchResult
|
||
if err := enhancedTwoLevelCache.GetSerializer().Deserialize(data, &results); err == nil {
|
||
// 返回缓存数据
|
||
fmt.Printf("✅ [%s] 命中缓存 结果数: %d\n", keyword, len(results))
|
||
return results, nil
|
||
} else {
|
||
displayKey := cacheKey[:8] + "..."
|
||
fmt.Printf("[主服务] 缓存反序列化失败: %s(关键词:%s) | 错误: %v\n", displayKey, keyword, err)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 缓存未命中或强制刷新,执行实际搜索
|
||
|
||
// 获取所有可用插件
|
||
var availablePlugins []plugin.AsyncSearchPlugin
|
||
if s.pluginManager != nil {
|
||
allPlugins := s.pluginManager.GetPlugins()
|
||
|
||
// 确保plugins不为nil并且有非空元素
|
||
hasPlugins := plugins != nil && len(plugins) > 0
|
||
hasNonEmptyPlugin := false
|
||
|
||
if hasPlugins {
|
||
for _, p := range plugins {
|
||
if p != "" {
|
||
hasNonEmptyPlugin = true
|
||
break
|
||
}
|
||
}
|
||
}
|
||
|
||
// 只有当plugins数组包含非空元素时才进行过滤
|
||
if hasPlugins && hasNonEmptyPlugin {
|
||
pluginMap := make(map[string]bool)
|
||
for _, p := range plugins {
|
||
if p != "" { // 忽略空字符串
|
||
pluginMap[strings.ToLower(p)] = true
|
||
}
|
||
}
|
||
|
||
for _, p := range allPlugins {
|
||
if pluginMap[strings.ToLower(p.Name())] {
|
||
availablePlugins = append(availablePlugins, p)
|
||
}
|
||
}
|
||
} else {
|
||
// 如果plugins为nil、空数组或只包含空字符串,视为未指定,使用所有插件
|
||
availablePlugins = allPlugins
|
||
}
|
||
}
|
||
|
||
// 控制并发数
|
||
if concurrency <= 0 {
|
||
// 使用配置中的默认值
|
||
concurrency = config.AppConfig.DefaultConcurrency
|
||
}
|
||
|
||
// 使用工作池执行并行搜索
|
||
tasks := make([]pool.Task, 0, len(availablePlugins))
|
||
for _, p := range availablePlugins {
|
||
plugin := p // 创建副本,避免闭包问题
|
||
tasks = append(tasks, func() interface{} {
|
||
// 设置主缓存键和当前关键词
|
||
plugin.SetMainCacheKey(cacheKey)
|
||
plugin.SetCurrentKeyword(keyword)
|
||
|
||
// 调用异步插件的AsyncSearch方法
|
||
results, err := plugin.AsyncSearch(keyword, func(client *http.Client, kw string, extParams map[string]interface{}) ([]model.SearchResult, error) {
|
||
// 使用插件的Search方法作为搜索函数
|
||
return plugin.Search(kw, extParams)
|
||
}, cacheKey, ext)
|
||
|
||
if err != nil {
|
||
return nil
|
||
}
|
||
return results
|
||
})
|
||
}
|
||
|
||
// 执行搜索任务并获取结果
|
||
results := pool.ExecuteBatchWithTimeout(tasks, concurrency, config.AppConfig.PluginTimeout)
|
||
|
||
// 合并所有插件的结果,过滤掉无链接的结果
|
||
var allResults []model.SearchResult
|
||
for _, result := range results {
|
||
if result != nil {
|
||
pluginResults := result.([]model.SearchResult)
|
||
// 只添加有链接的结果到最终结果中
|
||
for _, pluginResult := range pluginResults {
|
||
if len(pluginResult.Links) > 0 {
|
||
allResults = append(allResults, pluginResult)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 恢复主程序缓存更新:确保最终合并结果被正确缓存
|
||
if cacheInitialized && config.AppConfig.CacheEnabled {
|
||
go func(res []model.SearchResult, kw string, key string) {
|
||
ttl := time.Duration(config.AppConfig.CacheTTLMinutes) * time.Minute
|
||
|
||
// 使用增强版缓存,确保与异步插件使用相同的序列化器
|
||
if enhancedTwoLevelCache != nil {
|
||
data, err := enhancedTwoLevelCache.GetSerializer().Serialize(res)
|
||
if err != nil {
|
||
fmt.Printf("[主程序] 缓存序列化失败: %s | 错误: %v\n", key, err)
|
||
return
|
||
}
|
||
|
||
// 主程序最后更新,覆盖可能有问题的异步插件缓存
|
||
// 使用同步方式确保数据写入磁盘
|
||
enhancedTwoLevelCache.SetBothLevels(key, data, ttl)
|
||
if config.AppConfig != nil && config.AppConfig.AsyncLogEnabled {
|
||
fmt.Printf("[主程序] 缓存更新完成: %s | 结果数: %d",
|
||
key, len(res))
|
||
}
|
||
}
|
||
}(allResults, keyword, cacheKey)
|
||
}
|
||
|
||
return allResults, nil
|
||
}
|
||
|
||
|
||
|
||
// GetPluginManager 获取插件管理器
|
||
func (s *SearchService) GetPluginManager() *plugin.PluginManager {
|
||
return s.pluginManager
|
||
}
|
||
|
||
// =============================================================================
|
||
// 轻量级插件优先级排序实现
|
||
// =============================================================================
|
||
|
||
// ResultScore 搜索结果评分结构
|
||
type ResultScore struct {
|
||
Result model.SearchResult
|
||
TimeScore float64 // 时间得分
|
||
KeywordScore int // 关键词得分
|
||
PluginScore int // 插件等级得分
|
||
TotalScore float64 // 综合得分
|
||
}
|
||
|
||
// 插件等级缓存
|
||
var (
|
||
pluginLevelCache = sync.Map{} // 插件等级缓存
|
||
)
|
||
|
||
// getResultSource 从SearchResult推断数据来源
|
||
func getResultSource(result model.SearchResult) string {
|
||
if result.Channel != "" {
|
||
// 来自TG频道
|
||
return "tg:" + result.Channel
|
||
} else if result.UniqueID != "" && strings.Contains(result.UniqueID, "-") {
|
||
// 来自插件:UniqueID格式通常为 "插件名-ID"
|
||
parts := strings.SplitN(result.UniqueID, "-", 2)
|
||
if len(parts) >= 1 {
|
||
return "plugin:" + parts[0]
|
||
}
|
||
}
|
||
return "unknown"
|
||
}
|
||
|
||
// getPluginLevelBySource 根据来源获取插件等级
|
||
func getPluginLevelBySource(source string) int {
|
||
// 尝试从缓存获取
|
||
if level, ok := pluginLevelCache.Load(source); ok {
|
||
return level.(int)
|
||
}
|
||
|
||
parts := strings.Split(source, ":")
|
||
if len(parts) != 2 {
|
||
pluginLevelCache.Store(source, 3)
|
||
return 3 // 默认等级
|
||
}
|
||
|
||
if parts[0] == "tg" {
|
||
pluginLevelCache.Store(source, 3)
|
||
return 3 // TG搜索等同于等级3
|
||
}
|
||
|
||
if parts[0] == "plugin" {
|
||
level := getPluginPriorityByName(parts[1])
|
||
pluginLevelCache.Store(source, level)
|
||
return level
|
||
}
|
||
|
||
pluginLevelCache.Store(source, 3)
|
||
return 3
|
||
}
|
||
|
||
// getPluginPriorityByName 根据插件名获取优先级
|
||
func getPluginPriorityByName(pluginName string) int {
|
||
// 从插件管理器动态获取真实的优先级 (O(1)哈希查找)
|
||
if pluginInstance, exists := plugin.GetPluginByName(pluginName); exists {
|
||
return pluginInstance.Priority()
|
||
}
|
||
return 3 // 默认等级
|
||
}
|
||
|
||
// getPluginLevelScore 获取插件等级得分
|
||
func getPluginLevelScore(source string) int {
|
||
level := getPluginLevelBySource(source)
|
||
|
||
switch level {
|
||
case 1:
|
||
return 1000 // 等级1插件:1000分
|
||
case 2:
|
||
return 500 // 等级2插件:500分
|
||
case 3:
|
||
return 0 // 等级3插件:0分
|
||
case 4:
|
||
return -200 // 等级4插件:-200分
|
||
default:
|
||
return 0 // 默认使用等级3得分
|
||
}
|
||
}
|
||
|
||
// calculateTimeScore 计算时间得分
|
||
func calculateTimeScore(datetime time.Time) float64 {
|
||
if datetime.IsZero() {
|
||
return 0 // 无时间信息得0分
|
||
}
|
||
|
||
now := time.Now()
|
||
daysDiff := now.Sub(datetime).Hours() / 24
|
||
|
||
// 时间得分:越新得分越高,最大500分(增加时间权重)
|
||
switch {
|
||
case daysDiff <= 1:
|
||
return 500 // 1天内
|
||
case daysDiff <= 3:
|
||
return 400 // 3天内
|
||
case daysDiff <= 7:
|
||
return 300 // 1周内
|
||
case daysDiff <= 30:
|
||
return 200 // 1月内
|
||
case daysDiff <= 90:
|
||
return 100 // 3月内
|
||
case daysDiff <= 365:
|
||
return 50 // 1年内
|
||
default:
|
||
return 20 // 1年以上
|
||
}
|
||
}
|
||
|
||
|