支持注册插件路由到主程序

This commit is contained in:
www.xueximeng.com
2025-10-27 15:56:05 +08:00
parent ab2faba2b8
commit d057ef481e
11 changed files with 1001 additions and 992 deletions

View File

@@ -61,7 +61,8 @@ ENV CACHE_PATH=/app/cache \
ASYNC_MAX_BACKGROUND_WORKERS=20 \
ASYNC_MAX_BACKGROUND_TASKS=100 \
ASYNC_CACHE_TTL_HOURS=1 \
ENABLED_PLUGINS=labi,zhizhen,shandian,duoduo,muou,wanou \
CHANNELS=tgsearchers3,Aliyun_4K_Movies,bdbdndn11,yunpanx,bsbdbfjfjff,yp123pan,sbsbsnsqq,yunpanxunlei,tianyifc,BaiduCloudDisk,txtyzy,peccxinpd,gotopan,PanjClub,kkxlzy,baicaoZY,MCPH01,bdwpzhpd,ysxb48,jdjdn1111,yggpan,MCPH086,zaihuayun,Q66Share,ucwpzy,shareAliyun,alyp_1,dianyingshare,Quark_Movies,XiangxiuNBB,ydypzyfx,ucquark,xx123pan,yingshifenxiang123,zyfb123,tyypzhpd,tianyirigeng,cloudtianyi,hdhhd21,Lsp115,oneonefivewpfx,qixingzhenren,taoxgzy,Channel_Shares_115,tyysypzypd,vip115hot,wp123zy,yunpan139,yunpan189,yunpanuc,yydf_hzl,leoziyuan,pikpakpan,Q_dongman,yoyokuakeduanju,TG654TG,WFYSFX02,QukanMovie,yeqingjie_GJG666,movielover8888_film3,Baidu_netdisk,D_wusun,FLMdongtianfudi,KaiPanshare,QQZYDAPP,rjyxfx \
ENABLED_PLUGINS=labi,zhizhen,shandian,duoduo,muou,wanou,hunhepan,jikepan,panwiki,pansearch,panta,qupansou,hdr4k,pan666,susu,thepiratebay,xuexizhinan,panyq,ouge,huban,cyg,erxiao,miaoso,fox4k,pianku,clmao,wuji,cldi,xiaozhang,libvio,leijing,xb6v,xys,ddys,hdmoli,yuhuage,u3c3,javdb,clxiong,jutoushe,sdso,xiaoji,xdyh,haisou,bixin,djgou,nyaa,xinjuc,aikanzy,qupanshe,xdpan,discourse,yunsou \
AUTH_ENABLED=false \
AUTH_TOKEN_EXPIRY=24

View File

@@ -3,6 +3,7 @@ package api
import (
"github.com/gin-gonic/gin"
"pansou/config"
"pansou/plugin"
"pansou/service"
"pansou/util"
)
@@ -76,5 +77,13 @@ func SetupRouter(searchService *service.SearchService) *gin.Engine {
})
}
// 注册插件的Web路由如果插件实现了PluginWithWebHandler接口
allPlugins := plugin.GetRegisteredPlugins()
for _, p := range allPlugins {
if webPlugin, ok := p.(plugin.PluginWithWebHandler); ok {
webPlugin.RegisterWebRoutes(r.Group(""))
}
}
return r
}

View File

@@ -9,9 +9,9 @@ services:
- "8888:8888"
environment:
- PORT=8888
- CHANNELS=tgsearchers3,Aliyun_4K_Movies,bdbdndn11,yunpanx,bsbdbfjfjff,yp123pan,sbsbsnsqq,yunpanxunlei,tianyifc,BaiduCloudDisk,txtyzy,peccxinpd,gotopan,PanjClub,kkxlzy,baicaoZY,MCPH01,bdwpzhpd,ysxb48,jdjdn1111,yggpan,MCPH086,zaihuayun,Q66Share,Oscar_4Kmovies,ucwpzy,shareAliyun,alyp_1,dianyingshare,Quark_Movies,XiangxiuNBB,ydypzyfx,ucquark,xx123pan,yingshifenxiang123,zyfb123,tyypzhpd,tianyirigeng,cloudtianyi,hdhhd21,Lsp115,oneonefivewpfx,qixingzhenren,taoxgzy,Channel_Shares_115,tyysypzypd,vip115hot,wp123zy,yunpan139,yunpan189,yunpanuc,yydf_hzl,leoziyuan,pikpakpan,Q_dongman,yoyokuakeduanju
- CHANNELS=tgsearchers3,Aliyun_4K_Movies,bdbdndn11,yunpanx,bsbdbfjfjff,yp123pan,sbsbsnsqq,yunpanxunlei,tianyifc,BaiduCloudDisk,txtyzy,peccxinpd,gotopan,PanjClub,kkxlzy,baicaoZY,MCPH01,bdwpzhpd,ysxb48,jdjdn1111,yggpan,MCPH086,zaihuayun,Q66Share,ucwpzy,shareAliyun,alyp_1,dianyingshare,Quark_Movies,XiangxiuNBB,ydypzyfx,ucquark,xx123pan,yingshifenxiang123,zyfb123,tyypzhpd,tianyirigeng,cloudtianyi,hdhhd21,Lsp115,oneonefivewpfx,qixingzhenren,taoxgzy,Channel_Shares_115,tyysypzypd,vip115hot,wp123zy,yunpan139,yunpan189,yunpanuc,yydf_hzl,leoziyuan,pikpakpan,Q_dongman,yoyokuakeduanju,TG654TG,WFYSFX02,QukanMovie,yeqingjie_GJG666,movielover8888_film3,Baidu_netdisk,D_wusun,FLMdongtianfudi,KaiPanshare,QQZYDAPP,rjyxfx
# 必须指定启用的插件,多个插件用逗号分隔
- ENABLED_PLUGINS=labi,zhizhen,shandian,duoduo,muou,wanou
- ENABLED_PLUGINS=labi,zhizhen,shandian,duoduo,muou,wanou,hunhepan,jikepan,panwiki,pansearch,panta,qupansou,hdr4k,pan666,susu,thepiratebay,xuexizhinan,panyq,ouge,huban,cyg,erxiao,miaoso,fox4k,pianku,clmao,wuji,cldi,xiaozhang,libvio,leijing,xb6v,xys,ddys,hdmoli,yuhuage,u3c3,javdb,clxiong,jutoushe,sdso,xiaoji,xdyh,haisou,bixin,djgou,nyaa,xinjuc,aikanzy,qupanshe,xdpan,discourse,yunsou
- CACHE_ENABLED=true
- CACHE_PATH=/app/cache
- CACHE_MAX_SIZE=100

View File

@@ -25,8 +25,8 @@ import (
// 以下是插件的空导入用于触发各插件的init函数实现自动注册
// 添加新插件时,只需在此处添加对应的导入语句即可
// _ "pansou/plugin/hdr4k"
// _ "pansou/plugin/pan666"
_ "pansou/plugin/hdr4k"
_ "pansou/plugin/pan666"
_ "pansou/plugin/hunhepan"
_ "pansou/plugin/jikepan"
_ "pansou/plugin/panwiki"

View File

@@ -1,976 +0,0 @@
package plugin
import (
"fmt"
"net/http"
"strings"
"sync"
"sync/atomic"
"time"
"pansou/config"
"pansou/model"
)
// 工作池和统计相关变量
var (
// API响应缓存键为关键词值为缓存的响应仅内存不持久化
apiResponseCache = sync.Map{}
// 工作池相关变量
backgroundWorkerPool chan struct{}
backgroundTasksCount int32 = 0
// 统计数据 (仅用于内部监控)
cacheHits int64 = 0
cacheMisses int64 = 0
asyncCompletions int64 = 0
// 初始化标志
initialized bool = false
initLock sync.Mutex
// 默认配置值
defaultAsyncResponseTimeout = 4 * time.Second
defaultPluginTimeout = 30 * time.Second
defaultCacheTTL = 1 * time.Hour // 恢复但仅用于内存缓存
defaultMaxBackgroundWorkers = 20
defaultMaxBackgroundTasks = 100
// 缓存访问频率记录
cacheAccessCount = sync.Map{}
// 🔥 新增:缓存清理相关变量
lastCleanupTime = time.Now()
cleanupMutex sync.Mutex
)
// 缓存响应结构(仅内存,不持久化到磁盘)
type cachedResponse struct {
Results []model.SearchResult `json:"results"`
Timestamp time.Time `json:"timestamp"`
Complete bool `json:"complete"`
LastAccess time.Time `json:"last_access"`
AccessCount int `json:"access_count"`
}
// 🔥 新增清理过期API缓存的函数
func cleanupExpiredApiCache() {
cleanupMutex.Lock()
defer cleanupMutex.Unlock()
now := time.Now()
// 只有距离上次清理超过30分钟才执行
if now.Sub(lastCleanupTime) < 30*time.Minute {
return
}
cleanedCount := 0
totalCount := 0
deletedKeys := make([]string, 0)
// 清理已过期的缓存基于实际TTL + 合理的宽限期)
apiResponseCache.Range(func(key, value interface{}) bool {
totalCount++
if cached, ok := value.(cachedResponse); ok {
// 使用默认TTL + 30分钟宽限期避免过于激进的清理
expireThreshold := defaultCacheTTL + 30*time.Minute
if now.Sub(cached.Timestamp) > expireThreshold {
keyStr := key.(string)
apiResponseCache.Delete(key)
deletedKeys = append(deletedKeys, keyStr)
cleanedCount++
}
}
return true
})
// 清理访问计数缓存中对应的项
for _, key := range deletedKeys {
cacheAccessCount.Delete(key)
}
lastCleanupTime = now
// 记录清理日志(仅在有清理时输出)
if cleanedCount > 0 {
fmt.Printf("[Cache] 清理过期缓存: 删除 %d/%d 项,释放内存\n", cleanedCount, totalCount)
}
}
// initAsyncPlugin 初始化异步插件配置
func initAsyncPlugin() {
initLock.Lock()
defer initLock.Unlock()
if initialized {
return
}
// 如果配置已加载,则从配置读取工作池大小
maxWorkers := defaultMaxBackgroundWorkers
if config.AppConfig != nil {
maxWorkers = config.AppConfig.AsyncMaxBackgroundWorkers
}
backgroundWorkerPool = make(chan struct{}, maxWorkers)
// 异步插件本地缓存系统已移除,现在只依赖主缓存系统
initialized = true
}
// InitAsyncPluginSystem 导出的初始化函数,用于确保异步插件系统初始化
func InitAsyncPluginSystem() {
initAsyncPlugin()
}
// 缓存清理和持久化系统已移除
// 异步插件现在只负责搜索,缓存统一由主缓存系统管理
// 异步插件本地缓存系统已完全移除
// 现在异步插件只负责搜索,缓存统一由主缓存系统管理
// acquireWorkerSlot 尝试获取工作槽
func acquireWorkerSlot() bool {
// 获取最大任务数
maxTasks := int32(defaultMaxBackgroundTasks)
if config.AppConfig != nil {
maxTasks = int32(config.AppConfig.AsyncMaxBackgroundTasks)
}
// 检查总任务数
if atomic.LoadInt32(&backgroundTasksCount) >= maxTasks {
return false
}
// 尝试获取工作槽
select {
case backgroundWorkerPool <- struct{}{}:
atomic.AddInt32(&backgroundTasksCount, 1)
return true
default:
return false
}
}
// releaseWorkerSlot 释放工作槽
func releaseWorkerSlot() {
<-backgroundWorkerPool
atomic.AddInt32(&backgroundTasksCount, -1)
}
// recordCacheHit 记录缓存命中 (内部使用)
func recordCacheHit() {
atomic.AddInt64(&cacheHits, 1)
}
// recordCacheMiss 记录缓存未命中 (内部使用)
func recordCacheMiss() {
atomic.AddInt64(&cacheMisses, 1)
}
// recordAsyncCompletion 记录异步完成 (内部使用)
func recordAsyncCompletion() {
atomic.AddInt64(&asyncCompletions, 1)
}
// recordCacheAccess 记录缓存访问次数,用于智能缓存策略(仅内存)
func recordCacheAccess(key string) {
// 更新缓存项的访问时间和计数
if cached, ok := apiResponseCache.Load(key); ok {
cachedItem := cached.(cachedResponse)
cachedItem.LastAccess = time.Now()
cachedItem.AccessCount++
apiResponseCache.Store(key, cachedItem)
}
// 更新全局访问计数
if count, ok := cacheAccessCount.Load(key); ok {
cacheAccessCount.Store(key, count.(int) + 1)
} else {
cacheAccessCount.Store(key, 1)
}
// 🔥 新增:触发定期清理(异步执行,不阻塞当前操作)
go cleanupExpiredApiCache()
}
// BaseAsyncPlugin 基础异步插件结构(保留内存缓存,移除磁盘持久化)
type BaseAsyncPlugin struct {
name string
priority int
client *http.Client // 用于短超时的客户端
backgroundClient *http.Client // 用于长超时的客户端
cacheTTL time.Duration // 内存缓存有效期
mainCacheUpdater func(string, []model.SearchResult, time.Duration, bool, string) error // 主缓存更新函数支持IsFinal参数接收原始数据最后参数为关键词
MainCacheKey string // 主缓存键,导出字段
currentKeyword string // 当前搜索的关键词,用于日志显示
finalUpdateTracker map[string]bool // 追踪已更新的最终结果缓存
finalUpdateMutex sync.RWMutex // 保护finalUpdateTracker的并发访问
skipServiceFilter bool // 是否跳过Service层的关键词过滤
}
// NewBaseAsyncPlugin 创建基础异步插件
func NewBaseAsyncPlugin(name string, priority int) *BaseAsyncPlugin {
// 确保异步插件已初始化
if !initialized {
initAsyncPlugin()
}
// 确定超时和缓存时间
responseTimeout := defaultAsyncResponseTimeout
processingTimeout := defaultPluginTimeout
cacheTTL := defaultCacheTTL
// 如果配置已初始化,则使用配置中的值
if config.AppConfig != nil {
responseTimeout = config.AppConfig.AsyncResponseTimeoutDur
processingTimeout = config.AppConfig.PluginTimeout
cacheTTL = time.Duration(config.AppConfig.AsyncCacheTTLHours) * time.Hour
}
return &BaseAsyncPlugin{
name: name,
priority: priority,
client: &http.Client{
Timeout: responseTimeout,
},
backgroundClient: &http.Client{
Timeout: processingTimeout,
},
cacheTTL: cacheTTL,
finalUpdateTracker: make(map[string]bool), // 初始化缓存更新追踪器
skipServiceFilter: false, // 默认不跳过Service层过滤
}
}
// NewBaseAsyncPluginWithFilter 创建基础异步插件支持设置Service层过滤参数
func NewBaseAsyncPluginWithFilter(name string, priority int, skipServiceFilter bool) *BaseAsyncPlugin {
// 确保异步插件已初始化
if !initialized {
initAsyncPlugin()
}
// 确定超时和缓存时间
responseTimeout := defaultAsyncResponseTimeout
processingTimeout := defaultPluginTimeout
cacheTTL := defaultCacheTTL
// 如果配置已初始化,则使用配置中的值
if config.AppConfig != nil {
responseTimeout = config.AppConfig.AsyncResponseTimeoutDur
processingTimeout = config.AppConfig.PluginTimeout
cacheTTL = time.Duration(config.AppConfig.AsyncCacheTTLHours) * time.Hour
}
return &BaseAsyncPlugin{
name: name,
priority: priority,
client: &http.Client{
Timeout: responseTimeout,
},
backgroundClient: &http.Client{
Timeout: processingTimeout,
},
cacheTTL: cacheTTL,
finalUpdateTracker: make(map[string]bool), // 初始化缓存更新追踪器
skipServiceFilter: skipServiceFilter, // 使用传入的过滤设置
}
}
// SetMainCacheKey 设置主缓存键
func (p *BaseAsyncPlugin) SetMainCacheKey(key string) {
p.MainCacheKey = key
}
// SetCurrentKeyword 设置当前搜索关键词(用于日志显示)
func (p *BaseAsyncPlugin) SetCurrentKeyword(keyword string) {
p.currentKeyword = keyword
}
// SetMainCacheUpdater 设置主缓存更新函数(修复后的签名,增加关键词参数)
func (p *BaseAsyncPlugin) SetMainCacheUpdater(updater func(string, []model.SearchResult, time.Duration, bool, string) error) {
p.mainCacheUpdater = updater
}
// Name 返回插件名称
func (p *BaseAsyncPlugin) Name() string {
return p.name
}
// Priority 返回插件优先级
func (p *BaseAsyncPlugin) Priority() int {
return p.priority
}
// SkipServiceFilter 返回是否跳过Service层的关键词过滤
func (p *BaseAsyncPlugin) SkipServiceFilter() bool {
return p.skipServiceFilter
}
// AsyncSearch 异步搜索基础方法
func (p *BaseAsyncPlugin) AsyncSearch(
keyword string,
searchFunc func(*http.Client, string, map[string]interface{}) ([]model.SearchResult, error),
mainCacheKey string,
ext map[string]interface{},
) ([]model.SearchResult, error) {
// 确保ext不为nil
if ext == nil {
ext = make(map[string]interface{})
}
now := time.Now()
// 修改缓存键,确保包含插件名称
pluginSpecificCacheKey := fmt.Sprintf("%s:%s", p.name, keyword)
// 检查缓存
if cachedItems, ok := apiResponseCache.Load(pluginSpecificCacheKey); ok {
cachedResult := cachedItems.(cachedResponse)
// 缓存完全有效(未过期且完整)
if time.Since(cachedResult.Timestamp) < p.cacheTTL && cachedResult.Complete {
recordCacheHit()
recordCacheAccess(pluginSpecificCacheKey)
// 如果缓存接近过期已用时间超过TTL的80%),在后台刷新缓存
if time.Since(cachedResult.Timestamp) > (p.cacheTTL * 4 / 5) {
go p.refreshCacheInBackground(keyword, pluginSpecificCacheKey, searchFunc, cachedResult, mainCacheKey, ext)
}
return cachedResult.Results, nil
}
// 缓存已过期但有结果,启动后台刷新,同时返回旧结果
if len(cachedResult.Results) > 0 {
recordCacheHit()
recordCacheAccess(pluginSpecificCacheKey)
// 标记为部分过期
if time.Since(cachedResult.Timestamp) >= p.cacheTTL {
// 在后台刷新缓存
go p.refreshCacheInBackground(keyword, pluginSpecificCacheKey, searchFunc, cachedResult, mainCacheKey, ext)
// 日志记录
fmt.Printf("[%s] 缓存已过期,后台刷新中: %s (已过期: %v)\n",
p.name, pluginSpecificCacheKey, time.Since(cachedResult.Timestamp))
}
return cachedResult.Results, nil
}
}
recordCacheMiss()
// 创建通道
resultChan := make(chan []model.SearchResult, 1)
errorChan := make(chan error, 1)
doneChan := make(chan struct{})
// 启动后台处理
go func() {
// 尝试获取工作槽
if !acquireWorkerSlot() {
// 工作池已满,使用快速响应客户端直接处理
results, err := searchFunc(p.client, keyword, ext)
if err != nil {
select {
case errorChan <- err:
default:
}
return
}
select {
case resultChan <- results:
default:
}
// 缓存结果
apiResponseCache.Store(pluginSpecificCacheKey, cachedResponse{
Results: results,
Timestamp: now,
Complete: true,
LastAccess: now,
AccessCount: 1,
})
// 🔧 工作池满时短超时(默认4秒)内完成,这是完整结果
p.updateMainCacheWithFinal(mainCacheKey, results, true)
return
}
defer releaseWorkerSlot()
// 执行搜索
results, err := searchFunc(p.backgroundClient, keyword, ext)
// 检查是否已经响应
select {
case <-doneChan:
// 已经响应,只更新缓存
if err == nil {
// 检查是否存在旧缓存
var accessCount int = 1
var lastAccess time.Time = now
if oldCache, ok := apiResponseCache.Load(pluginSpecificCacheKey); ok {
oldCachedResult := oldCache.(cachedResponse)
accessCount = oldCachedResult.AccessCount
lastAccess = oldCachedResult.LastAccess
// 合并结果(新结果优先)
if len(oldCachedResult.Results) > 0 {
// 创建合并结果集
mergedResults := make([]model.SearchResult, 0, len(results) + len(oldCachedResult.Results))
// 创建已有结果ID的映射
existingIDs := make(map[string]bool)
for _, r := range results {
existingIDs[r.UniqueID] = true
mergedResults = append(mergedResults, r)
}
// 添加旧结果中不存在的项
for _, r := range oldCachedResult.Results {
if !existingIDs[r.UniqueID] {
mergedResults = append(mergedResults, r)
}
}
// 使用合并结果
results = mergedResults
}
}
apiResponseCache.Store(pluginSpecificCacheKey, cachedResponse{
Results: results,
Timestamp: now,
Complete: true,
LastAccess: lastAccess,
AccessCount: accessCount,
})
recordAsyncCompletion()
// 异步插件后台完成时更新主缓存(标记为最终结果)
p.updateMainCacheWithFinal(mainCacheKey, results, true)
// 异步插件本地缓存系统已移除
}
default:
// 尚未响应,发送结果
if err != nil {
select {
case errorChan <- err:
default:
}
} else {
// 检查是否存在旧缓存用于合并
if oldCache, ok := apiResponseCache.Load(pluginSpecificCacheKey); ok {
oldCachedResult := oldCache.(cachedResponse)
if len(oldCachedResult.Results) > 0 {
// 创建合并结果集
mergedResults := make([]model.SearchResult, 0, len(results) + len(oldCachedResult.Results))
// 创建已有结果ID的映射
existingIDs := make(map[string]bool)
for _, r := range results {
existingIDs[r.UniqueID] = true
mergedResults = append(mergedResults, r)
}
// 添加旧结果中不存在的项
for _, r := range oldCachedResult.Results {
if !existingIDs[r.UniqueID] {
mergedResults = append(mergedResults, r)
}
}
// 使用合并结果
results = mergedResults
}
}
select {
case resultChan <- results:
default:
}
// 更新缓存
apiResponseCache.Store(pluginSpecificCacheKey, cachedResponse{
Results: results,
Timestamp: now,
Complete: true,
LastAccess: now,
AccessCount: 1,
})
// 🔧 短超时(默认4秒)内正常完成,这是完整的最终结果
p.updateMainCacheWithFinal(mainCacheKey, results, true)
// 异步插件本地缓存系统已移除
}
}
}()
// 获取响应超时时间
responseTimeout := defaultAsyncResponseTimeout
if config.AppConfig != nil {
responseTimeout = config.AppConfig.AsyncResponseTimeoutDur
}
// 等待响应超时或结果
select {
case results := <-resultChan:
close(doneChan)
return results, nil
case err := <-errorChan:
close(doneChan)
return nil, err
case <-time.After(responseTimeout):
// 插件响应超时,后台继续处理(优化完成,日志简化)
// 响应超时,返回空结果,后台继续处理
go func() {
defer close(doneChan)
}()
// 检查是否有部分缓存可用
if cachedItems, ok := apiResponseCache.Load(pluginSpecificCacheKey); ok {
cachedResult := cachedItems.(cachedResponse)
if len(cachedResult.Results) > 0 {
// 有部分缓存可用,记录访问并返回
recordCacheAccess(pluginSpecificCacheKey)
fmt.Printf("[%s] 响应超时,返回部分缓存: %s (项目数: %d)\n",
p.name, pluginSpecificCacheKey, len(cachedResult.Results))
return cachedResult.Results, nil
}
}
// 创建空的临时缓存,以便后台处理完成后可以更新
apiResponseCache.Store(pluginSpecificCacheKey, cachedResponse{
Results: []model.SearchResult{},
Timestamp: now,
Complete: false, // 标记为不完整
LastAccess: now,
AccessCount: 1,
})
// 🔧 修复4秒超时时也要更新主缓存标记为部分结果空结果
p.updateMainCacheWithFinal(mainCacheKey, []model.SearchResult{}, false)
// fmt.Printf("[%s] 响应超时,后台继续处理: %s\n", p.name, pluginSpecificCacheKey)
return []model.SearchResult{}, nil
}
}
// AsyncSearchWithResult 异步搜索方法返回PluginSearchResult
func (p *BaseAsyncPlugin) AsyncSearchWithResult(
keyword string,
searchFunc func(*http.Client, string, map[string]interface{}) ([]model.SearchResult, error),
mainCacheKey string,
ext map[string]interface{},
) (model.PluginSearchResult, error) {
// 确保ext不为nil
if ext == nil {
ext = make(map[string]interface{})
}
now := time.Now()
// 修改缓存键,确保包含插件名称
pluginSpecificCacheKey := fmt.Sprintf("%s:%s", p.name, keyword)
// 检查缓存
if cachedItems, ok := apiResponseCache.Load(pluginSpecificCacheKey); ok {
cachedResult := cachedItems.(cachedResponse)
// 缓存完全有效(未过期且完整)
if time.Since(cachedResult.Timestamp) < p.cacheTTL && cachedResult.Complete {
recordCacheHit()
recordCacheAccess(pluginSpecificCacheKey)
// 如果缓存接近过期已用时间超过TTL的80%),在后台刷新缓存
if time.Since(cachedResult.Timestamp) > (p.cacheTTL * 4 / 5) {
go p.refreshCacheInBackground(keyword, pluginSpecificCacheKey, searchFunc, cachedResult, mainCacheKey, ext)
}
return model.PluginSearchResult{
Results: cachedResult.Results,
IsFinal: cachedResult.Complete,
Timestamp: cachedResult.Timestamp,
Source: p.name,
Message: "从缓存获取",
}, nil
}
// 缓存已过期但有结果,启动后台刷新,同时返回旧结果
if len(cachedResult.Results) > 0 {
recordCacheHit()
recordCacheAccess(pluginSpecificCacheKey)
// 标记为部分过期
if time.Since(cachedResult.Timestamp) >= p.cacheTTL {
// 在后台刷新缓存
go p.refreshCacheInBackground(keyword, pluginSpecificCacheKey, searchFunc, cachedResult, mainCacheKey, ext)
}
return model.PluginSearchResult{
Results: cachedResult.Results,
IsFinal: false, // 🔥 过期数据标记为非最终结果
Timestamp: cachedResult.Timestamp,
Source: p.name,
Message: "缓存已过期,后台刷新中",
}, nil
}
}
recordCacheMiss()
// 创建通道
resultChan := make(chan []model.SearchResult, 1)
errorChan := make(chan error, 1)
doneChan := make(chan struct{})
// 启动后台处理
go func() {
defer func() {
select {
case <-doneChan:
default:
close(doneChan)
}
}()
// 尝试获取工作槽
if !acquireWorkerSlot() {
// 工作池已满,使用快速响应客户端直接处理
results, err := searchFunc(p.client, keyword, ext)
if err != nil {
select {
case errorChan <- err:
default:
}
return
}
select {
case resultChan <- results:
default:
}
return
}
defer releaseWorkerSlot()
// 使用长超时客户端进行搜索
results, err := searchFunc(p.backgroundClient, keyword, ext)
if err != nil {
select {
case errorChan <- err:
default:
}
} else {
select {
case resultChan <- results:
default:
}
}
}()
// 等待结果或超时
responseTimeout := defaultAsyncResponseTimeout
if config.AppConfig != nil {
responseTimeout = config.AppConfig.AsyncResponseTimeoutDur
}
select {
case results := <-resultChan:
// 不直接关闭让defer处理
// 缓存结果
apiResponseCache.Store(pluginSpecificCacheKey, cachedResponse{
Results: results,
Timestamp: now,
Complete: true, // 🔥 及时完成,标记为完整结果
LastAccess: now,
AccessCount: 1,
})
// 🔧 恢复主缓存更新使用统一的GOB序列化
// 传递原始数据,由主程序负责序列化
if mainCacheKey != "" && p.mainCacheUpdater != nil {
err := p.mainCacheUpdater(mainCacheKey, results, p.cacheTTL, true, p.currentKeyword)
if err != nil {
fmt.Printf("❌ [%s] 及时完成缓存更新失败: %s | 错误: %v\n", p.name, mainCacheKey, err)
}
}
return model.PluginSearchResult{
Results: results,
IsFinal: true, // 🔥 及时完成,最终结果
Timestamp: now,
Source: p.name,
Message: "搜索完成",
}, nil
case err := <-errorChan:
// 不直接关闭让defer处理
return model.PluginSearchResult{}, err
case <-time.After(responseTimeout):
// 🔥 超时处理:返回空结果,后台继续处理
go p.completeSearchInBackground(keyword, searchFunc, pluginSpecificCacheKey, mainCacheKey, doneChan, ext)
// 存储临时缓存(标记为不完整)
apiResponseCache.Store(pluginSpecificCacheKey, cachedResponse{
Results: []model.SearchResult{},
Timestamp: now,
Complete: false, // 🔥 标记为不完整
LastAccess: now,
AccessCount: 1,
})
return model.PluginSearchResult{
Results: []model.SearchResult{},
IsFinal: false, // 🔥 超时返回,非最终结果
Timestamp: now,
Source: p.name,
Message: "处理中,后台继续...",
}, nil
}
}
// completeSearchInBackground 后台完成搜索
func (p *BaseAsyncPlugin) completeSearchInBackground(
keyword string,
searchFunc func(*http.Client, string, map[string]interface{}) ([]model.SearchResult, error),
pluginCacheKey string,
mainCacheKey string,
doneChan chan struct{},
ext map[string]interface{},
) {
defer func() {
select {
case <-doneChan:
default:
close(doneChan)
}
}()
// 执行完整搜索
results, err := searchFunc(p.backgroundClient, keyword, ext)
if err != nil {
return
}
// 更新插件缓存
now := time.Now()
apiResponseCache.Store(pluginCacheKey, cachedResponse{
Results: results,
Timestamp: now,
Complete: true, // 🔥 标记为完整结果
LastAccess: now,
AccessCount: 1,
})
// 🔧 恢复主缓存更新使用统一的GOB序列化
// 传递原始数据,由主程序负责序列化
if mainCacheKey != "" && p.mainCacheUpdater != nil {
err := p.mainCacheUpdater(mainCacheKey, results, p.cacheTTL, true, p.currentKeyword)
if err != nil {
fmt.Printf("❌ [%s] 后台完成缓存更新失败: %s | 错误: %v\n", p.name, mainCacheKey, err)
}
}
}
// refreshCacheInBackground 在后台刷新缓存
func (p *BaseAsyncPlugin) refreshCacheInBackground(
keyword string,
cacheKey string,
searchFunc func(*http.Client, string, map[string]interface{}) ([]model.SearchResult, error),
oldCache cachedResponse,
originalCacheKey string,
ext map[string]interface{},
) {
// 确保ext不为nil
if ext == nil {
ext = make(map[string]interface{})
}
// 注意这里的cacheKey已经是插件特定的了因为是从AsyncSearch传入的
// 检查是否有足够的工作槽
if !acquireWorkerSlot() {
return
}
defer releaseWorkerSlot()
// 记录刷新开始时间
refreshStart := time.Now()
// 执行搜索
results, err := searchFunc(p.backgroundClient, keyword, ext)
if err != nil || len(results) == 0 {
return
}
// 创建合并结果集
mergedResults := make([]model.SearchResult, 0, len(results) + len(oldCache.Results))
// 创建已有结果ID的映射
existingIDs := make(map[string]bool)
for _, r := range results {
existingIDs[r.UniqueID] = true
mergedResults = append(mergedResults, r)
}
// 添加旧结果中不存在的项
for _, r := range oldCache.Results {
if !existingIDs[r.UniqueID] {
mergedResults = append(mergedResults, r)
}
}
// 更新缓存
apiResponseCache.Store(cacheKey, cachedResponse{
Results: mergedResults,
Timestamp: time.Now(),
Complete: true,
LastAccess: oldCache.LastAccess,
AccessCount: oldCache.AccessCount,
})
// 🔥 异步插件后台刷新完成时更新主缓存(标记为最终结果)
p.updateMainCacheWithFinal(originalCacheKey, mergedResults, true)
// 记录刷新时间
refreshTime := time.Since(refreshStart)
fmt.Printf("[%s] 后台刷新完成: %s (耗时: %v, 新项目: %d, 合并项目: %d)\n",
p.name, cacheKey, refreshTime, len(results), len(mergedResults))
// 异步插件本地缓存系统已移除
}
// updateMainCache 更新主缓存系统兼容性方法默认IsFinal=true
func (p *BaseAsyncPlugin) updateMainCache(cacheKey string, results []model.SearchResult) {
p.updateMainCacheWithFinal(cacheKey, results, true)
}
// updateMainCacheWithFinal 更新主缓存系统支持IsFinal参数
func (p *BaseAsyncPlugin) updateMainCacheWithFinal(cacheKey string, results []model.SearchResult, isFinal bool) {
// 如果主缓存更新函数为空或缓存键为空,直接返回
if p.mainCacheUpdater == nil || cacheKey == "" {
return
}
// 🚀 优化:如果新结果为空,跳过缓存更新(避免无效操作)
if len(results) == 0 {
return
}
// 🔥 增强防重复更新机制 - 使用数据哈希确保真正的去重
// 生成结果数据的简单哈希标识
dataHash := fmt.Sprintf("%d_%d", len(results), results[0].UniqueID)
if len(results) > 1 {
dataHash += fmt.Sprintf("_%d", results[len(results)-1].UniqueID)
}
updateKey := fmt.Sprintf("final_%s_%s_%s_%t", p.name, cacheKey, dataHash, isFinal)
// 检查是否已经处理过相同的数据
if p.hasUpdatedFinalCache(updateKey) {
return
}
// 标记已更新
p.markFinalCacheUpdated(updateKey)
// 🔧 恢复异步插件缓存更新,使用修复后的统一序列化
// 传递原始数据由主程序负责GOB序列化
if p.mainCacheUpdater != nil {
err := p.mainCacheUpdater(cacheKey, results, p.cacheTTL, isFinal, p.currentKeyword)
if err != nil {
fmt.Printf("❌ [%s] 主缓存更新失败: %s | 错误: %v\n", p.name, cacheKey, err)
}
}
}
// FilterResultsByKeyword 根据关键词过滤搜索结果
func (p *BaseAsyncPlugin) FilterResultsByKeyword(results []model.SearchResult, keyword string) []model.SearchResult {
if keyword == "" {
return results
}
// 预估过滤后会保留80%的结果
filteredResults := make([]model.SearchResult, 0, len(results)*8/10)
// 将关键词转为小写,用于不区分大小写的比较
lowerKeyword := strings.ToLower(keyword)
// 将关键词按空格分割,用于支持多关键词搜索
keywords := strings.Fields(lowerKeyword)
for _, result := range results {
// 将标题和内容转为小写
lowerTitle := strings.ToLower(result.Title)
lowerContent := strings.ToLower(result.Content)
// 检查每个关键词是否在标题或内容中
matched := true
for _, kw := range keywords {
// 对于所有关键词,检查是否在标题或内容中
if !strings.Contains(lowerTitle, kw) && !strings.Contains(lowerContent, kw) {
matched = false
break
}
}
if matched {
filteredResults = append(filteredResults, result)
}
}
return filteredResults
}
// GetClient 返回短超时客户端
func (p *BaseAsyncPlugin) GetClient() *http.Client {
return p.client
}
// hasUpdatedFinalCache 检查是否已经更新过指定的最终结果缓存
func (p *BaseAsyncPlugin) hasUpdatedFinalCache(updateKey string) bool {
p.finalUpdateMutex.RLock()
defer p.finalUpdateMutex.RUnlock()
return p.finalUpdateTracker[updateKey]
}
// markFinalCacheUpdated 标记已更新指定的最终结果缓存
func (p *BaseAsyncPlugin) markFinalCacheUpdated(updateKey string) {
p.finalUpdateMutex.Lock()
defer p.finalUpdateMutex.Unlock()
p.finalUpdateTracker[updateKey] = true
}
// 全局序列化器引用(由主程序设置)
var globalCacheSerializer interface {
Serialize(interface{}) ([]byte, error)
Deserialize([]byte, interface{}) error
}
// SetGlobalCacheSerializer 设置全局缓存序列化器(由主程序调用)
func SetGlobalCacheSerializer(serializer interface {
Serialize(interface{}) ([]byte, error)
Deserialize([]byte, interface{}) error
}) {
globalCacheSerializer = serializer
}
// getEnhancedCacheSerializer 获取增强缓存的序列化器
func getEnhancedCacheSerializer() interface {
Serialize(interface{}) ([]byte, error)
Deserialize([]byte, interface{}) error
} {
return globalCacheSerializer
}

View File

@@ -84,7 +84,7 @@ func (p *BixinAsyncPlugin) doSearch(client *http.Client, keyword string, ext map
uniqueResults := p.deduplicateResults(allResults)
// 使用过滤功能过滤结果
filteredResults := p.FilterResultsByKeyword(uniqueResults, keyword)
filteredResults := plugin.FilterResultsByKeyword(uniqueResults, keyword)
return filteredResults, nil
}

View File

@@ -33,7 +33,8 @@ var (
// 常量定义
const (
pluginName = "discourse"
searchURLTemplate = "https://linux.do/search.json?q=%s%%20%%23resource%%3Acloud-asset%%20in%%3Atitle&page=%d"
// searchURLTemplate = "https://linux.do/search.json?q=%s%%20%%23resource%%3Acloud-asset%%20in%%3Atitle&page=%d"
searchURLTemplate = "https://linux.do/search.json?q=%s%%20in%%3Atitle%%20%%23resource&page=%d"
detailURLTemplate = "https://linux.do/t/%d.json?track_visit=true&forceLoad=true"
defaultPriority = 2
defaultTimeout = 30 * time.Second

View File

@@ -84,7 +84,7 @@ func (p *Pan666AsyncPlugin) doSearch(client *http.Client, keyword string, ext ma
uniqueResults := p.deduplicateResults(allResults)
// 使用过滤功能过滤结果
filteredResults := p.FilterResultsByKeyword(uniqueResults, keyword)
filteredResults := plugin.FilterResultsByKeyword(uniqueResults, keyword)
return filteredResults, nil
}

View File

@@ -362,7 +362,7 @@ func (p *PanyqPlugin) doSearch(client *http.Client, keyword string, ext map[stri
}
// 使用关键词过滤结果
filteredResults := p.FilterResultsByKeyword(results, keyword)
filteredResults := plugin.FilterResultsByKeyword(results, keyword)
if DebugLog {
fmt.Println("panyq: returning", len(filteredResults), "filtered results")

File diff suppressed because it is too large Load Diff

View File

@@ -242,7 +242,7 @@ func (p *XuexizhinanPlugin) doSearch(client *http.Client, keyword string, ext ma
// 如果有结果,返回结果;如果没有结果,但有错误,返回第一个错误
if len(results) > 0 {
// 使用过滤功能过滤结果
filteredResults := p.FilterResultsByKeyword(results, keyword)
filteredResults := plugin.FilterResultsByKeyword(results, keyword)
return filteredResults, nil
} else if len(errs) > 0 {