支持ext自定义扩展参数

This commit is contained in:
www.xueximeng.com
2025-07-22 12:12:21 +08:00
parent 42fbcb06aa
commit 9664df1ffa
19 changed files with 639 additions and 72 deletions

View File

@@ -7,12 +7,13 @@ PanSou是一个高性能的网盘资源搜索API服务支持TG搜索和自定
- **高性能搜索**并发搜索多个Telegram频道显著提升搜索速度工作池设计高效管理并发任务
- **网盘类型分类**:自动识别多种网盘链接,按类型归类展示
- **智能排序**:基于时间和关键词权重的多级排序策略
- **异步插件系统**:支持通过插件扩展搜索来源,已内置多个网盘搜索插件,详情参考[插件开发指南.md](docs/插件开发指南.md);支持"尽快响应,持续处理"的异步搜索模,解决了某些搜索源响应时间长的问题
- **异步插件系统**:支持通过插件扩展搜索来源,已内置多个网盘搜索插件,详情参考[插件开发指南.md](docs/插件开发指南.md);支持"尽快响应,持续处理"的异步搜索模,解决了某些搜索源响应时间长的问题
- **双级超时控制**:短超时(4秒)确保快速响应,长超时(30秒)允许完整处理
- **持久化缓存**:缓存自动保存到磁盘,系统重启后自动恢复
- **优雅关闭**:在程序退出前保存缓存,确保数据不丢失
- **增量更新**:智能合并新旧结果,保留有价值的数据
- **主动更新**:异步插件在缓存异步更新后会主动更新主缓存(内存+磁盘),使用户在不强制刷新的情况下也能获取最新数据
- **插件扩展参数**通过ext参数向插件传递自定义搜索参数如英文标题、全量搜索标志等提高搜索灵活性和精确度
- **二级缓存**:内存+分片磁盘缓存机制,大幅提升重复查询速度和并发性能
- **分片磁盘缓存**:将缓存数据分散到多个子目录,减少锁竞争,通过哈希算法将缓存键均匀分布到不同分片,提高高并发场景下的性能
- **序列化器接口**Gob序列化提供更高性能和更小的结果大小
@@ -218,6 +219,7 @@ server {
| res | string | 否 | 结果类型all(返回所有结果)、results(仅返回results)、merge(仅返回merged_by_type)默认为merge |
| src | string | 否 | 数据来源类型all(默认,全部来源)、tg(仅Telegram)、plugin(仅插件) |
| plugins | string[] | 否 | 指定搜索的插件列表,不指定则搜索全部插件 |
| ext | object | 否 | 扩展参数,用于传递给插件的自定义参数,如{"title_en":"English Title", "is_all":true} |
**GET请求参数**
@@ -230,6 +232,7 @@ server {
| res | string | 否 | 结果类型all(返回所有结果)、results(仅返回results)、merge(仅返回merged_by_type)默认为merge |
| src | string | 否 | 数据来源类型all(默认,全部来源)、tg(仅Telegram)、plugin(仅插件) |
| plugins | string | 否 | 指定搜索的插件列表,使用英文逗号分隔多个插件名,不指定则搜索全部插件 |
| ext | string | 否 | JSON格式的扩展参数用于传递给插件的自定义参数如{"title_en":"English Title", "is_all":true} |
**POST请求示例**
@@ -241,14 +244,18 @@ server {
"refresh": true,
"res": "merge",
"src": "all",
"plugins": ["jikepan"]
"plugins": ["jikepan"],
"ext": {
"title_en": "Fast and Furious",
"is_all": true
}
}
```
**GET请求示例**
```
GET /api/search?kw=速度与激情&channels=tgsearchers2,xxx&conc=2&refresh=true&res=merge&src=tg
GET /api/search?kw=速度与激情&channels=tgsearchers2,xxx&conc=2&refresh=true&res=merge&src=tg&ext={"title_en":"Fast and Furious","is_all":true}
```
**成功响应**

View File

@@ -90,6 +90,25 @@ func SearchHandler(c *gin.Context) {
plugins = nil
}
// 处理ext参数JSON格式
var ext map[string]interface{}
extStr := c.Query("ext")
if extStr != "" && extStr != " " {
// 处理特殊情况ext={}
if extStr == "{}" {
ext = make(map[string]interface{})
} else {
if err := jsonutil.Unmarshal([]byte(extStr), &ext); err != nil {
c.JSON(http.StatusBadRequest, model.NewErrorResponse(400, "无效的ext参数格式: "+err.Error()))
return
}
}
}
// 确保ext不为nil
if ext == nil {
ext = make(map[string]interface{})
}
req = model.SearchRequest{
Keyword: keyword,
Channels: channels,
@@ -98,6 +117,7 @@ func SearchHandler(c *gin.Context) {
ResultType: resultType,
SourceType: sourceType,
Plugins: plugins,
Ext: ext,
}
} else {
// POST方式从请求体获取
@@ -144,7 +164,7 @@ func SearchHandler(c *gin.Context) {
}
// 执行搜索
result, err := searchService.Search(req.Keyword, req.Channels, req.Concurrency, req.ForceRefresh, req.ResultType, req.SourceType, req.Plugins)
result, err := searchService.Search(req.Keyword, req.Channels, req.Concurrency, req.ForceRefresh, req.ResultType, req.SourceType, req.Plugins, req.Ext)
if err != nil {
response := model.NewErrorResponse(500, "搜索失败: "+err.Error())

View File

@@ -59,6 +59,7 @@ PanSou采用模块化的分层架构设计主要包括以下几个层次
- **响应生成**生成标准化的JSON响应
- **中间件**:跨域处理、日志记录、压缩等
- **参数规范化**:统一处理不同形式但语义相同的参数
- **扩展参数处理**:支持通过`ext`参数向插件传递自定义搜索参数
#### 2.2.2 服务层
@@ -67,6 +68,7 @@ PanSou采用模块化的分层架构设计主要包括以下几个层次
- **缓存管理**管理搜索结果的缓存策略支持TG和插件搜索的独立缓存
- **缓存键生成**:基于所有影响结果的参数生成一致的缓存键
- **主缓存注入**:将主缓存系统注入到异步插件中,实现统一的缓存更新
- **参数传递**将自定义参数从API层传递到插件层
#### 2.2.3 插件系统
@@ -75,6 +77,7 @@ PanSou采用模块化的分层架构设计主要包括以下几个层次
- **自动注册**通过init函数实现插件自动注册
- **高性能JSON处理**使用sonic库优化JSON序列化/反序列化
- **异步插件**:支持"尽快响应,持续处理"的异步搜索模式
- **扩展参数支持**:通过`ext`参数支持插件自定义搜索参数
#### 2.2.4 工具层

View File

@@ -140,6 +140,25 @@ func SearchHandler(c *gin.Context) {
}
}
// 处理ext参数JSON格式
var ext map[string]interface{}
extStr := c.Query("ext")
if extStr != "" && extStr != " " {
// 处理特殊情况ext={}
if extStr == "{}" {
ext = make(map[string]interface{})
} else {
if err := jsonutil.Unmarshal([]byte(extStr), &ext); err != nil {
c.JSON(http.StatusBadRequest, model.NewErrorResponse(400, "无效的ext参数格式: "+err.Error()))
return
}
}
}
// 确保ext不为nil
if ext == nil {
ext = make(map[string]interface{})
}
req = model.SearchRequest{
Keyword: keyword,
Channels: channels,
@@ -148,6 +167,7 @@ func SearchHandler(c *gin.Context) {
ResultType: resultType,
SourceType: sourceType,
Plugins: plugins,
Ext: ext,
}
} else {
// POST方式从请求体获取
@@ -189,7 +209,7 @@ func SearchHandler(c *gin.Context) {
}
// 执行搜索
result, err := searchService.Search(req.Keyword, req.Channels, req.Concurrency, req.ForceRefresh, req.ResultType, req.SourceType, req.Plugins)
result, err := searchService.Search(req.Keyword, req.Channels, req.Concurrency, req.ForceRefresh, req.ResultType, req.SourceType, req.Plugins, req.Ext)
if err != nil {
response := model.NewErrorResponse(500, "搜索失败: "+err.Error())
@@ -299,6 +319,7 @@ func LoggerMiddleware() gin.HandlerFunc {
| res | string | 否 | 结果类型all(返回所有结果)、results(仅返回results)、merge(仅返回merged_by_type)默认为merge |
| src | string | 否 | 数据来源类型all(默认,全部来源)、tg(仅Telegram)、plugin(仅插件) |
| plugins | string[] | 否 | 指定搜索的插件列表,不指定则搜索全部插件 |
| ext | object | 否 | 扩展参数,用于传递给插件的自定义参数,如{"title_en":"English Title", "is_all":true} |
#### GET请求参数
@@ -311,6 +332,7 @@ func LoggerMiddleware() gin.HandlerFunc {
| res | string | 否 | 结果类型all(返回所有结果)、results(仅返回results)、merge(仅返回merged_by_type)默认为merge |
| src | string | 否 | 数据来源类型all(默认,全部来源)、tg(仅Telegram)、plugin(仅插件) |
| plugins | string | 否 | 指定搜索的插件列表,使用英文逗号分隔多个插件名,不指定则搜索全部插件 |
| ext | string | 否 | JSON格式的扩展参数用于传递给插件的自定义参数如{"title_en":"English Title", "is_all":true} |
#### 成功响应
@@ -410,7 +432,30 @@ if c.Request.URL.Query().Has("plugins") {
}
```
### 7.2 参数互斥与规范化处理
### 7.2 扩展参数处理
```go
// 处理ext参数JSON格式
var ext map[string]interface{}
extStr := c.Query("ext")
if extStr != "" && extStr != " " {
// 处理特殊情况ext={}
if extStr == "{}" {
ext = make(map[string]interface{})
} else {
if err := jsonutil.Unmarshal([]byte(extStr), &ext); err != nil {
c.JSON(http.StatusBadRequest, model.NewErrorResponse(400, "无效的ext参数格式: "+err.Error()))
return
}
}
}
// 确保ext不为nil
if ext == nil {
ext = make(map[string]interface{})
}
```
### 7.3 参数互斥与规范化处理
```go
// 参数互斥逻辑当src=tg时忽略plugins参数当src=plugin时忽略channels参数

View File

@@ -82,7 +82,12 @@ func NewSearchService(pluginManager *plugin.PluginManager) *SearchService {
```go
// Search 执行搜索
func (s *SearchService) Search(keyword string, channels []string, concurrency int, forceRefresh bool, resultType string, sourceType string, plugins []string) (model.SearchResponse, error) {
func (s *SearchService) Search(keyword string, channels []string, concurrency int, forceRefresh bool, resultType string, sourceType string, plugins []string, ext map[string]interface{}) (model.SearchResponse, error) {
// 确保ext不为nil
if ext == nil {
ext = make(map[string]interface{})
}
// 参数预处理
// 源类型标准化
if sourceType == "" {
@@ -175,7 +180,7 @@ func (s *SearchService) Search(keyword string, channels []string, concurrency in
defer wg.Done()
// 对于插件搜索,我们总是希望获取最新的缓存数据
// 因此即使forceRefresh=false我们也需要确保获取到最新的缓存
pluginResults, pluginErr = s.searchPlugins(keyword, plugins, forceRefresh, concurrency)
pluginResults, pluginErr = s.searchPlugins(keyword, plugins, forceRefresh, concurrency, ext)
}()
}
@@ -369,7 +374,12 @@ func (s *SearchService) searchTG(keyword string, channels []string, forceRefresh
```go
// searchPlugins 搜索插件
func (s *SearchService) searchPlugins(keyword string, plugins []string, forceRefresh bool, concurrency int) ([]model.SearchResult, error) {
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{})
}
// 生成缓存键
cacheKey := cache.GeneratePluginCacheKey(keyword, plugins)
@@ -456,7 +466,7 @@ func (s *SearchService) searchPlugins(keyword string, plugins []string, forceRef
for _, p := range availablePlugins {
plugin := p // 创建副本,避免闭包问题
tasks = append(tasks, func() interface{} {
results, err := plugin.Search(keyword)
results, err := plugin.Search(keyword, ext)
if err != nil {
return nil
}

View File

@@ -31,7 +31,8 @@ type SearchPlugin interface {
Name() string
// Search 执行搜索并返回结果
Search(keyword string) ([]model.SearchResult, error)
// ext参数用于传递额外的搜索参数插件可以根据需要使用或忽略
Search(keyword string, ext map[string]interface{}) ([]model.SearchResult, error)
// Priority 返回插件优先级(可选,用于控制结果排序)
Priority() int
@@ -122,7 +123,7 @@ func (p *JikePanPlugin) Name() string {
}
// Search 执行搜索
func (p *JikePanPlugin) Search(keyword string) ([]model.SearchResult, error) {
func (p *JikePanPlugin) Search(keyword string, ext map[string]interface{}) ([]model.SearchResult, error) {
// 实现搜索逻辑
// ...
return results, nil
@@ -194,9 +195,15 @@ func (p *BaseAsyncPlugin) SetMainCacheUpdater(updater func(string, []byte, time.
// AsyncSearch 异步搜索基础方法
func (p *BaseAsyncPlugin) AsyncSearch(
keyword string,
searchFunc func(*http.Client, string) ([]model.SearchResult, error),
searchFunc func(*http.Client, string, map[string]interface{}) ([]model.SearchResult, error),
mainCacheKey string, // 主缓存key参数
ext map[string]interface{}, // 扩展参数
) ([]model.SearchResult, error) {
// 确保ext不为nil
if ext == nil {
ext = make(map[string]interface{})
}
now := time.Now()
// 修改缓存键,确保包含插件名称
@@ -244,7 +251,7 @@ func (p *BaseAsyncPlugin) AsyncSearch(
// 尝试获取工作槽
if !acquireWorkerSlot() {
// 工作池已满,使用快速响应客户端直接处理
results, err := searchFunc(p.client, keyword)
results, err := searchFunc(p.client, keyword, ext)
// 处理结果...
// 更新主缓存系统
@@ -255,7 +262,7 @@ func (p *BaseAsyncPlugin) AsyncSearch(
defer releaseWorkerSlot()
// 执行搜索
results, err := searchFunc(p.backgroundClient, keyword)
results, err := searchFunc(p.backgroundClient, keyword, ext)
// 检查是否已经响应
select {
@@ -322,8 +329,9 @@ func (p *BaseAsyncPlugin) SetMainCacheKey(key string) {
// AsyncSearch 异步搜索基础方法
func (p *BaseAsyncPlugin) AsyncSearch(
keyword string,
searchFunc func(*http.Client, string) ([]model.SearchResult, error),
searchFunc func(*http.Client, string, map[string]interface{}) ([]model.SearchResult, error),
mainCacheKey string, // 主缓存key参数
ext map[string]interface{}, // 扩展参数
) ([]model.SearchResult, error) {
// ...
@@ -381,13 +389,22 @@ func NewMyAsyncPlugin() *MyAsyncPlugin {
}
// Search 实现搜索接口
func (p *MyAsyncPlugin) Search(keyword string) ([]model.SearchResult, error) {
// 使用保存的主缓存键
return p.AsyncSearch(keyword, p.doSearch, p.MainCacheKey)
func (p *MyAsyncPlugin) Search(keyword string, ext map[string]interface{}) ([]model.SearchResult, error) {
// 使用保存的主缓存键传递ext参数
return p.BaseAsyncPlugin.AsyncSearch(keyword, p.doSearch, p.MainCacheKey, ext)
}
// doSearch 执行实际搜索
func (p *MyAsyncPlugin) doSearch(client *http.Client, keyword string) ([]model.SearchResult, error) {
func (p *MyAsyncPlugin) doSearch(client *http.Client, keyword string, ext map[string]interface{}) ([]model.SearchResult, error) {
// 处理ext参数
if ext != nil {
// 根据需要使用ext参数
if customParam, ok := ext["custom_param"].(string); ok && customParam != "" {
// 使用自定义参数
keyword = fmt.Sprintf("%s %s", keyword, customParam)
}
}
// 实现搜索逻辑
// ...
return results, nil
@@ -524,7 +541,16 @@ func (p *MyPlugin) Name() string {
}
// Search 执行搜索
func (p *MyPlugin) Search(keyword string) ([]model.SearchResult, error) {
func (p *MyPlugin) Search(keyword string, ext map[string]interface{}) ([]model.SearchResult, error) {
// 处理ext参数
if ext != nil {
// 根据需要使用ext参数
if customParam, ok := ext["custom_param"].(string); ok && customParam != "" {
// 使用自定义参数
keyword = fmt.Sprintf("%s %s", keyword, customParam)
}
}
// 实现搜索逻辑
// ...
return results, nil
@@ -564,13 +590,13 @@ func NewMyAsyncPlugin() *MyAsyncPlugin {
}
// Search 实现搜索接口
func (p *MyAsyncPlugin) Search(keyword string) ([]model.SearchResult, error) {
func (p *MyAsyncPlugin) Search(keyword string, ext map[string]interface{}) ([]model.SearchResult, error) {
// 使用保存的主缓存键
return p.BaseAsyncPlugin.AsyncSearch(keyword, p.doSearch, p.MainCacheKey)
return p.BaseAsyncPlugin.AsyncSearch(keyword, p.doSearch, p.MainCacheKey, ext)
}
// doSearch 执行实际搜索
func (p *MyAsyncPlugin) doSearch(client *http.Client, keyword string) ([]model.SearchResult, error) {
func (p *MyAsyncPlugin) doSearch(client *http.Client, keyword string, ext map[string]interface{}) ([]model.SearchResult, error) {
// 实现搜索逻辑
// ...
return results, nil

View File

@@ -539,6 +539,9 @@ PanSou采用了以下缓存键设计策略
- 异步插件和主缓存系统使用相同格式的缓存键
- 确保缓存一致性
5. **扩展参数处理**
- 扩展参数ext不参与缓存键生成避免缓存爆炸问题
### 4.4 列表参数处理
```go

View File

@@ -36,7 +36,8 @@ type SearchPlugin interface {
Name() string
// Search 执行搜索并返回结果
Search(keyword string) ([]model.SearchResult, error)
// ext参数用于传递额外的搜索参数插件可以根据需要使用或忽略
Search(keyword string, ext map[string]interface{}) ([]model.SearchResult, error)
// Priority 返回插件优先级(用于控制结果排序)
Priority() int
@@ -50,9 +51,10 @@ type SearchPlugin interface {
- 名称应简洁明了,全小写,不含特殊字符
- 例如:`pansearch``hunhepan``jikepan`
2. **Search(keyword string)**
2. **Search(keyword string, ext map[string]interface{})**
- 执行搜索并返回结果
- 参数 `keyword` 是用户输入的搜索关键词
- 参数 `ext` 是扩展参数,用于传递额外的搜索参数,如 `title_en`(英文标题)
- 返回值是搜索结果数组和可能的错误
- 实现时应处理超时和错误,确保不会无限阻塞
@@ -932,6 +934,39 @@ if datetime.IsZero() {
}
```
### 6. 扩展参数处理
- 正确处理ext参数提供额外搜索功能
- 始终检查ext是否为nil避免空指针异常
- 使用类型断言安全地获取参数值
- 在处理ext参数时保持向后兼容性
```go
// 处理ext参数
if ext != nil {
// 使用类型断言安全地获取参数
if titleEn, ok := ext["title_en"].(string); ok && titleEn != "" {
// 使用英文标题替换关键词
searchKeyword = titleEn
}
// 处理年份参数
if year, ok := ext["year"].(float64); ok && year > 0 {
// 将年份添加到搜索条件中
searchKeyword = fmt.Sprintf("%s %d", searchKeyword, int(year))
} else if yearStr, ok := ext["year"].(string); ok && yearStr != "" {
// 处理字符串形式的年份
searchKeyword = fmt.Sprintf("%s %s", searchKeyword, yearStr)
}
// 处理质量参数
if quality, ok := ext["quality"].(string); ok && quality != "" {
// 将质量添加到搜索条件中
searchKeyword = fmt.Sprintf("%s %s", searchKeyword, quality)
}
}
```
## 示例插件
以下是一个完整的示例插件实现:
@@ -994,9 +1029,19 @@ func (p *ExamplePlugin) Priority() int {
}
// Search 执行搜索并返回结果
func (p *ExamplePlugin) Search(keyword string) ([]model.SearchResult, error) {
func (p *ExamplePlugin) Search(keyword string, ext map[string]interface{}) ([]model.SearchResult, error) {
// 处理ext参数
searchKeyword := keyword
if ext != nil {
// 使用类型断言安全地获取参数
if titleEn, ok := ext["title_en"].(string); ok && titleEn != "" {
// 使用英文标题替换关键词
searchKeyword = titleEn
}
}
// 构建请求URL
reqURL := fmt.Sprintf("%s?q=%s", ApiURL, url.QueryEscape(keyword))
reqURL := fmt.Sprintf("%s?q=%s", ApiURL, url.QueryEscape(searchKeyword))
// 发送请求
req, err := http.NewRequest("GET", reqURL, nil)

View File

@@ -0,0 +1,373 @@
# 插件扩展参数设计与过滤逻辑重构
## 1. 背景与目标
### 1.1 背景
在PanSou搜索系统中在处理自定义搜索参数如英文标题搜索时存在局限性因为主程序只考虑原始关键词而不考虑插件可能使用的其他搜索参数。
### 1.2 目标
1. 支持通过`ext`参数向插件传递自定义搜索参数
2. 保持缓存机制的简单高效ext参数不参与缓存键生成
3. 允许各插件根据自身需求处理ext参数
## 2. 设计方案
### 2.1 插件接口扩展
扩展`SearchPlugin`接口的`Search`方法,添加`ext`参数:
```go
type SearchPlugin interface {
// Name 返回插件名称
Name() string
// Search 执行搜索并返回结果
// ext参数用于传递额外的搜索参数插件可以根据需要使用或忽略
Search(keyword string, ext map[string]interface{}) ([]model.SearchResult, error)
// Priority 返回插件优先级(可选,用于控制结果排序)
Priority() int
}
```
### 2.2 插件实现扩展
#### 2.2.1 普通插件实现
普通插件直接在Search方法中处理ext参数
```go
// Search 执行搜索并返回结果
func (p *SomePlugin) Search(keyword string, ext map[string]interface{}) ([]model.SearchResult, error) {
// 直接使用keyword进行搜索默认忽略ext参数
// 处理ext参数
// if ext != nil {
// if titleEn, ok := ext["title_en"].(string); ok && titleEn != "" {
// keyword = titleEn // 使用英文标题替换关键词
// }
// // 处理其他自定义参数...
// }
// 执行搜索逻辑...
return results, nil
}
```
#### 2.2.2 异步插件实现
异步插件需要修改`doSearch`方法签名添加ext参数
```go
// Search 执行搜索并返回结果
func (p *JikepanAsyncV2Plugin) Search(keyword string, ext map[string]interface{}) ([]model.SearchResult, error) {
// 使用保存的主缓存键传递ext参数
return p.AsyncSearch(keyword, p.doSearch, p.MainCacheKey, ext)
}
// doSearch 实际的搜索实现添加ext参数
func (p *JikepanAsyncV2Plugin) doSearch(client *http.Client, keyword string, ext map[string]interface{}) ([]model.SearchResult, error) {
// 处理ext参数
if ext != nil {
if titleEn, ok := ext["title_en"].(string); ok && titleEn != "" {
keyword = titleEn // 使用英文标题替换关键词
}
}
// 构建请求
reqBody := map[string]interface{}{
"name": keyword,
"is_all": false,
}
// 其余搜索逻辑...
return results, nil
}
```
#### 2.2.3 不使用ext参数的插件
对于不需要使用ext参数的插件可以简单地忽略该参数
```go
// Search 执行搜索并返回结果
func (p *SomePlugin) Search(keyword string, ext map[string]interface{}) ([]model.SearchResult, error) {
// 直接使用keyword进行搜索忽略ext参数
// ...
return results, nil
}
```
### 2.3 API层支持
`api/handler.go`中的`SearchHandler`函数中,添加对`ext`参数的处理:
```go
// 处理ext参数JSON格式
var ext map[string]interface{}
extStr := c.Query("ext")
if extStr != "" && extStr != " " {
// 处理特殊情况ext={}
if extStr == "{}" {
ext = make(map[string]interface{})
} else {
if err := jsonutil.Unmarshal([]byte(extStr), &ext); err != nil {
c.JSON(http.StatusBadRequest, model.NewErrorResponse(400, "无效的ext参数格式: "+err.Error()))
return
}
}
}
// 确保ext不为nil
if ext == nil {
ext = make(map[string]interface{})
}
req = model.SearchRequest{
Keyword: keyword,
Channels: channels,
Concurrency: concurrency,
ForceRefresh: forceRefresh,
ResultType: resultType,
SourceType: sourceType,
Plugins: plugins,
Ext: ext,
}
```
### 2.4 BaseAsyncPlugin 修改
修改`BaseAsyncPlugin.AsyncSearch`方法签名添加ext参数
```go
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{})
}
// 在调用searchFunc时传递ext参数
results, err := searchFunc(p.client, keyword, ext)
// ...其余逻辑保持不变
}
```
## 3. 实施步骤
### 3.1 阶段一:基础架构修改
1. **更新SearchRequest模型**
-`model/request.go`中的`SearchRequest`结构体添加`Ext map[string]interface{}`字段
2. **更新API处理函数**
- 修改`api/handler.go`中的`SearchHandler`函数添加ext参数处理
- 确保在GET和POST两种请求方式下都能正确处理ext参数
3. **修改SearchPlugin接口**
- 更新`plugin/plugin.go`中的`SearchPlugin`接口,修改`Search`方法签名
4. **修改BaseAsyncPlugin**
- 更新`plugin/baseasyncplugin.go`中的`AsyncSearch`方法签名和实现
- 确保在调用searchFunc时传递ext参数
### 3.2 阶段二:插件实现更新
1. **更新普通插件**
- 修改所有实现`SearchPlugin`接口的普通插件,更新`Search`方法签名
2. **更新异步插件**
- 修改所有异步插件的`doSearch`方法签名添加ext参数
- 更新异步插件的`Search`方法传递ext参数给`AsyncSearch`
3. **更新SearchService**
- 修改`service/search_service.go`中的`searchPlugins`方法传递ext参数给插件
### 3.3 阶段三:测试与优化
1. **单元测试**
- 为ext参数处理添加单元测试确保参数正确传递
- 测试不同类型的ext参数值字符串、数字、布尔值、数组、对象等
2. **集成测试**
- 测试API接口传递ext参数
- 测试不同插件对ext参数的处理
3. **性能测试**
- 确保添加ext参数后不影响系统性能
- 验证缓存机制仍然有效
## 4. 注意事项
### 4.1 缓存键生成
缓存键生成只使用原始关键词,不包含`ext`参数。这是一个有意的设计决策可以提高缓存命中率但也意味着使用不同ext参数的搜索可能共享相同缓存。
### 4.2 参数传递
确保ext参数在整个调用链中正确传递从API层到Service层再到各个插件实现。
### 4.3 插件兼容性
所有插件都需要更新接口实现,没有向后兼容的考虑,这是一次全面的改造。
## 5. 扩展性考虑
### 5.1 支持更多自定义参数
本设计使用通用的`map[string]interface{}`类型作为`ext`参数,可以灵活支持各种类型的自定义参数,如:
- `title_en`: 英文标题搜索
- `year`: 年份过滤
- `category`: 分类过滤
- `quality`: 质量过滤如1080p、4K等
### 5.2 插件特定参数
不同插件可以根据自身特点支持不同的自定义参数,主程序不需要了解这些参数的具体含义,只负责传递。
## 6. 示例实现
### 6.1 SearchRequest 模型更新
```go
// SearchRequest 搜索请求参数
type SearchRequest struct {
Keyword string `json:"kw" binding:"required"`
Channels []string `json:"channels"`
Concurrency int `json:"conc"`
ForceRefresh bool `json:"refresh"`
ResultType string `json:"res"`
SourceType string `json:"src"`
Plugins []string `json:"plugins"`
Ext map[string]interface{} `json:"ext"`
}
```
### 6.2 异步插件实现示例
```go
package myplugin
import (
"pansou/model"
"pansou/plugin"
)
// MyAsyncPlugin 自定义异步插件
type MyAsyncPlugin struct {
*plugin.BaseAsyncPlugin
}
// Search 实现搜索接口
func (p *MyAsyncPlugin) Search(keyword string, ext map[string]interface{}) ([]model.SearchResult, error) {
// 使用保存的主缓存键传递ext参数
return p.AsyncSearch(keyword, p.doSearch, p.MainCacheKey, ext)
}
// doSearch 执行实际搜索
func (p *MyAsyncPlugin) doSearch(client *http.Client, keyword string, ext map[string]interface{}) ([]model.SearchResult, error) {
// 处理ext参数
if ext != nil {
if titleEn, ok := ext["title_en"].(string); ok && titleEn != "" {
keyword = titleEn // 使用英文标题
}
// 其他自定义参数处理...
}
// 实现搜索逻辑
// ...
return results, nil
}
```
### 6.3 实际应用示例jikepan_ext插件
jikepan_ext插件是一个实际应用ext参数的例子它使用ext中的title_en参数来替代原始关键词进行搜索
```go
// doSearch 实际的搜索实现
func (p *JikepanAsyncV2Plugin) doSearch(client *http.Client, keyword string, ext map[string]interface{}) ([]model.SearchResult, error) {
// 构建请求
reqBody := map[string]interface{}{
"name": keyword,
"is_all": false,
}
// 检查ext中是否包含title_en参数如果有则使用它
if ext != nil {
if titleEn, ok := ext["title_en"].(string); ok && titleEn != "" {
// 使用title_en替换或补充keyword
reqBody["name"] = titleEn
}
}
jsonData, err := json.Marshal(reqBody)
if err != nil {
return nil, fmt.Errorf("marshal request failed: %w", err)
}
// 后续搜索逻辑...
}
```
这个实现允许客户端通过ext参数传递英文标题例如
```
GET /api/search?kw=火影忍者&ext={"title_en":"Naruto"}
```
在这个请求中jikepan_ext插件将使用"Naruto"而不是"火影忍者"作为搜索关键词,这对于搜索外文资源特别有用。
## 7. 使用示例
### 7.1 API请求示例
#### GET请求
```
GET /api/search?kw=火影忍者&ext={"title_en":"Naruto","year":2002}
```
#### POST请求
```json
POST /api/search
{
"kw": "火影忍者",
"ext": {
"title_en": "Naruto",
"year": 2002,
"quality": "1080p"
}
}
```
### 7.2 客户端使用示例
```javascript
// 使用fetch API发送带ext参数的请求
async function search(keyword, ext) {
const params = new URLSearchParams();
params.append('kw', keyword);
if (ext) {
params.append('ext', JSON.stringify(ext));
}
const response = await fetch(`/api/search?${params}`);
return await response.json();
}
// 使用示例
search('火影忍者', { title_en: 'Naruto', year: 2002 })
.then(results => {
console.log(results);
});
```

View File

@@ -2,11 +2,12 @@ package model
// SearchRequest 搜索请求参数
type SearchRequest struct {
Keyword string `json:"kw" binding:"required"` // 搜索关键词
Channels []string `json:"channels"` // 搜索的频道列表
Concurrency int `json:"conc"` // 并发搜索数量
ForceRefresh bool `json:"refresh"` // 强制刷新,不使用缓存
ResultType string `json:"res"` // 结果类型all(返回所有结果)、results(仅返回results)、merge(仅返回merged_by_type)
SourceType string `json:"src"` // 数据来源类型all(默认,全部来源)、tg(仅Telegram)、plugin(仅插件)
Plugins []string `json:"plugins"` // 指定搜索的插件列表,不指定则搜索全部插件
Keyword string `json:"kw" binding:"required"` // 搜索关键词
Channels []string `json:"channels"` // 搜索的频道列表
Concurrency int `json:"conc"` // 并发搜索数量
ForceRefresh bool `json:"refresh"` // 强制刷新,不使用缓存
ResultType string `json:"res"` // 结果类型all(返回所有结果)、results(仅返回results)、merge(仅返回merged_by_type)
SourceType string `json:"src"` // 数据来源类型all(默认,全部来源)、tg(仅Telegram)、plugin(仅插件)
Plugins []string `json:"plugins"` // 指定搜索的插件列表,不指定则搜索全部插件
Ext map[string]interface{} `json:"ext"` // 扩展参数,用于传递给插件的自定义参数
}

View File

@@ -526,9 +526,15 @@ func (p *BaseAsyncPlugin) Priority() int {
// AsyncSearch 异步搜索基础方法
func (p *BaseAsyncPlugin) AsyncSearch(
keyword string,
searchFunc func(*http.Client, string) ([]model.SearchResult, error),
mainCacheKey string, // 主缓存key参数
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()
// 修改缓存键,确保包含插件名称
@@ -545,7 +551,7 @@ func (p *BaseAsyncPlugin) AsyncSearch(
// 如果缓存接近过期已用时间超过TTL的80%),在后台刷新缓存
if time.Since(cachedResult.Timestamp) > (p.cacheTTL * 4 / 5) {
go p.refreshCacheInBackground(keyword, pluginSpecificCacheKey, searchFunc, cachedResult, mainCacheKey)
go p.refreshCacheInBackground(keyword, pluginSpecificCacheKey, searchFunc, cachedResult, mainCacheKey, ext)
}
return cachedResult.Results, nil
@@ -559,7 +565,7 @@ func (p *BaseAsyncPlugin) AsyncSearch(
// 标记为部分过期
if time.Since(cachedResult.Timestamp) >= p.cacheTTL {
// 在后台刷新缓存
go p.refreshCacheInBackground(keyword, pluginSpecificCacheKey, searchFunc, cachedResult, mainCacheKey)
go p.refreshCacheInBackground(keyword, pluginSpecificCacheKey, searchFunc, cachedResult, mainCacheKey, ext)
// 日志记录
fmt.Printf("[%s] 缓存已过期,后台刷新中: %s (已过期: %v)\n",
@@ -582,7 +588,7 @@ func (p *BaseAsyncPlugin) AsyncSearch(
// 尝试获取工作槽
if !acquireWorkerSlot() {
// 工作池已满,使用快速响应客户端直接处理
results, err := searchFunc(p.client, keyword)
results, err := searchFunc(p.client, keyword, ext)
if err != nil {
select {
case errorChan <- err:
@@ -613,7 +619,7 @@ func (p *BaseAsyncPlugin) AsyncSearch(
defer releaseWorkerSlot()
// 执行搜索
results, err := searchFunc(p.backgroundClient, keyword)
results, err := searchFunc(p.backgroundClient, keyword, ext)
// 检查是否已经响应
select {
@@ -775,10 +781,16 @@ func (p *BaseAsyncPlugin) AsyncSearch(
func (p *BaseAsyncPlugin) refreshCacheInBackground(
keyword string,
cacheKey string,
searchFunc func(*http.Client, string) ([]model.SearchResult, error),
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传入的
// 检查是否有足够的工作槽
@@ -791,7 +803,7 @@ func (p *BaseAsyncPlugin) refreshCacheInBackground(
refreshStart := time.Now()
// 执行搜索
results, err := searchFunc(p.backgroundClient, keyword)
results, err := searchFunc(p.backgroundClient, keyword, ext)
if err != nil || len(results) == 0 {
return
}

View File

@@ -43,13 +43,13 @@ func NewHunhepanAsyncPlugin() *HunhepanAsyncPlugin {
}
// Search 执行搜索并返回结果
func (p *HunhepanAsyncPlugin) Search(keyword string) ([]model.SearchResult, error) {
// 使用保存的主缓存键
return p.AsyncSearch(keyword, p.doSearch, p.MainCacheKey)
func (p *HunhepanAsyncPlugin) Search(keyword string, ext map[string]interface{}) ([]model.SearchResult, error) {
// 使用保存的主缓存键传递ext参数但不使用
return p.AsyncSearch(keyword, p.doSearch, p.MainCacheKey, ext)
}
// doSearch 实际的搜索实现
func (p *HunhepanAsyncPlugin) doSearch(client *http.Client, keyword string) ([]model.SearchResult, error) {
func (p *HunhepanAsyncPlugin) doSearch(client *http.Client, keyword string, ext map[string]interface{}) ([]model.SearchResult, error) {
// 创建结果通道和错误通道
resultChan := make(chan []HunhepanItem, 3)
errChan := make(chan error, 3)

View File

@@ -7,7 +7,7 @@ import (
"net/http"
"strings"
"time"
"log"
"pansou/model"
"pansou/plugin"
"pansou/util/json"
@@ -37,19 +37,30 @@ func NewJikepanAsyncV2Plugin() *JikepanAsyncV2Plugin {
}
// Search 执行搜索并返回结果
func (p *JikepanAsyncV2Plugin) Search(keyword string) ([]model.SearchResult, error) {
// 使用保存的主缓存键
return p.AsyncSearch(keyword, p.doSearch, p.MainCacheKey)
func (p *JikepanAsyncV2Plugin) Search(keyword string, ext map[string]interface{}) ([]model.SearchResult, error) {
// 使用保存的主缓存键传递ext参数但不使用
return p.AsyncSearch(keyword, p.doSearch, p.MainCacheKey, ext)
}
// doSearch 实际的搜索实现
func (p *JikepanAsyncV2Plugin) doSearch(client *http.Client, keyword string) ([]model.SearchResult, error) {
func (p *JikepanAsyncV2Plugin) doSearch(client *http.Client, keyword string, ext map[string]interface{}) ([]model.SearchResult, error) {
// 构建请求
reqBody := map[string]interface{}{
"name": keyword,
"is_all": false,
}
// 检查ext中是否包含title_en参数如果有则使用它
log.Printf("1111111111111111ext: v+%v", ext)
if ext != nil {
log.Printf("ext: v+%v", ext)
if isAll, ok := ext["is_all"].(bool); ok && isAll {
log.Printf("使用全量搜索 v+%v", isAll)
// 使用全量搜索时间大约10秒
reqBody["is_all"] = true
}
}
jsonData, err := json.Marshal(reqBody)
if err != nil {
return nil, fmt.Errorf("marshal request failed: %w", err)

View File

@@ -50,19 +50,19 @@ type Pan666AsyncPlugin struct {
// NewPan666AsyncPlugin 创建新的pan666异步插件
func NewPan666AsyncPlugin() *Pan666AsyncPlugin {
return &Pan666AsyncPlugin{
BaseAsyncPlugin: plugin.NewBaseAsyncPlugin("pan666", 3),
BaseAsyncPlugin: plugin.NewBaseAsyncPlugin("pan666", 4),
retries: MaxRetries,
}
}
// Search 执行搜索并返回结果
func (p *Pan666AsyncPlugin) Search(keyword string) ([]model.SearchResult, error) {
// 使用保存的主缓存键
return p.AsyncSearch(keyword, p.doSearch, p.MainCacheKey)
func (p *Pan666AsyncPlugin) Search(keyword string, ext map[string]interface{}) ([]model.SearchResult, error) {
// 使用保存的主缓存键传递ext参数但不使用
return p.AsyncSearch(keyword, p.doSearch, p.MainCacheKey, ext)
}
// doSearch 实际的搜索实现
func (p *Pan666AsyncPlugin) doSearch(client *http.Client, keyword string) ([]model.SearchResult, error) {
func (p *Pan666AsyncPlugin) doSearch(client *http.Client, keyword string, ext map[string]interface{}) ([]model.SearchResult, error) {
// 初始化随机数种子
rand.Seed(time.Now().UnixNano())

View File

@@ -514,7 +514,7 @@ func (p *PanSearchPlugin) getBaseURL() (string, error) {
}
// Search 执行搜索并返回结果
func (p *PanSearchPlugin) Search(keyword string) ([]model.SearchResult, error) {
func (p *PanSearchPlugin) Search(keyword string, ext map[string]interface{}) ([]model.SearchResult, error) {
// 生成缓存键
cacheKey := keyword

View File

@@ -252,7 +252,7 @@ func (p *PantaPlugin) Priority() int {
}
// Search 执行搜索并返回结果
func (p *PantaPlugin) Search(keyword string) ([]model.SearchResult, error) {
func (p *PantaPlugin) Search(keyword string, ext map[string]interface{}) ([]model.SearchResult, error) {
// 对关键词进行URL编码
encodedKeyword := url.QueryEscape(keyword)

View File

@@ -19,7 +19,8 @@ type SearchPlugin interface {
Name() string
// Search 执行搜索并返回结果
Search(keyword string) ([]model.SearchResult, error)
// ext参数用于传递额外的搜索参数插件可以根据需要使用或忽略
Search(keyword string, ext map[string]interface{}) ([]model.SearchResult, error)
// Priority 返回插件优先级(可选,用于控制结果排序)
Priority() int

View File

@@ -88,7 +88,7 @@ func (p *QuPanSouPlugin) Priority() int {
}
// Search 执行搜索并返回结果
func (p *QuPanSouPlugin) Search(keyword string) ([]model.SearchResult, error) {
func (p *QuPanSouPlugin) Search(keyword string, ext map[string]interface{}) ([]model.SearchResult, error) {
// 生成缓存键
cacheKey := keyword

View File

@@ -103,7 +103,12 @@ func injectMainCacheToAsyncPlugins(pluginManager *plugin.PluginManager, mainCach
}
// Search 执行搜索
func (s *SearchService) Search(keyword string, channels []string, concurrency int, forceRefresh bool, resultType string, sourceType string, plugins []string) (model.SearchResponse, error) {
func (s *SearchService) Search(keyword string, channels []string, concurrency int, forceRefresh bool, resultType string, sourceType string, plugins []string, ext map[string]interface{}) (model.SearchResponse, error) {
// 确保ext不为nil
if ext == nil {
ext = make(map[string]interface{})
}
// 参数预处理
// 源类型标准化
if sourceType == "" {
@@ -195,7 +200,7 @@ func (s *SearchService) Search(keyword string, channels []string, concurrency in
defer wg.Done()
// 对于插件搜索,我们总是希望获取最新的缓存数据
// 因此即使forceRefresh=false我们也需要确保获取到最新的缓存
pluginResults, pluginErr = s.searchPlugins(keyword, plugins, forceRefresh, concurrency)
pluginResults, pluginErr = s.searchPlugins(keyword, plugins, forceRefresh, concurrency, ext)
}()
}
@@ -557,7 +562,12 @@ func (s *SearchService) searchTG(keyword string, channels []string, forceRefresh
}
// searchPlugins 搜索插件
func (s *SearchService) searchPlugins(keyword string, plugins []string, forceRefresh bool, concurrency int) ([]model.SearchResult, error) {
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{})
}
// 生成缓存键
cacheKey := cache.GeneratePluginCacheKey(keyword, plugins)
@@ -648,25 +658,25 @@ func (s *SearchService) searchPlugins(keyword string, plugins []string, forceRef
tasks = append(tasks, func() interface{} {
// 检查插件是否为异步插件
if asyncPlugin, ok := plugin.(interface {
AsyncSearch(keyword string, searchFunc func(*http.Client, string) ([]model.SearchResult, error), mainCacheKey string) ([]model.SearchResult, error)
AsyncSearch(keyword string, searchFunc func(*http.Client, string, map[string]interface{}) ([]model.SearchResult, error), mainCacheKey string, ext map[string]interface{}) ([]model.SearchResult, error)
SetMainCacheKey(string)
}); ok {
// 先设置主缓存键
asyncPlugin.SetMainCacheKey(cacheKey)
// 是异步插件调用AsyncSearch方法并传递主缓存键
results, err := asyncPlugin.AsyncSearch(keyword, func(client *http.Client, kw string) ([]model.SearchResult, error) {
// 这里使用插件的Search方法作为搜索函数
return plugin.Search(kw)
}, cacheKey)
// 是异步插件调用AsyncSearch方法并传递主缓存键和ext参数
results, err := asyncPlugin.AsyncSearch(keyword, func(client *http.Client, kw string, extParams map[string]interface{}) ([]model.SearchResult, error) {
// 这里使用插件的Search方法作为搜索函数传递ext参数
return plugin.Search(kw, extParams)
}, cacheKey, ext)
if err != nil {
return nil
}
return results
} else {
// 不是异步插件直接调用Search方法
results, err := plugin.Search(keyword)
// 不是异步插件直接调用Search方法传递ext参数
results, err := plugin.Search(keyword, ext)
if err != nil {
return nil
}