/api/health返回当前加载的插件和频道

This commit is contained in:
www.xueximeng.com
2025-07-17 15:50:53 +08:00
parent 00b31565ff
commit d006913bca
3 changed files with 695 additions and 3 deletions

View File

@@ -196,7 +196,20 @@ GET /api/search?kw=速度与激情&channels=tgsearchers2,xxx&conc=2&refresh=true
```json
{
"status": "ok",
"channels": [
"tgsearchers2"
],
"plugin_count": 6,
"plugins": [
"pansearch",
"panta",
"qupansou",
"hunhepan",
"jikepan",
"pan666"
],
"plugins_enabled": true,
"status": "ok"
}
```
@@ -251,7 +264,7 @@ PanSou实现了高级异步插件系统解决了某些搜索源响应时间
### 异步插件特性
- **双级超时控制**:短超时(2秒)确保快速响应,长超时(30秒)允许完整处理
- **双级超时控制**:短超时(4秒)确保快速响应,长超时(30秒)允许完整处理
- **持久化缓存**:缓存自动保存到磁盘,系统重启后自动恢复
- **即时保存**:缓存更新后立即触发保存,不再等待定时器
- **优雅关闭**:在程序退出前保存缓存,确保数据不丢失

View File

@@ -2,6 +2,7 @@ package api
import (
"github.com/gin-gonic/gin"
"pansou/config"
"pansou/service"
"pansou/util"
)
@@ -31,15 +32,26 @@ func SetupRouter(searchService *service.SearchService) *gin.Engine {
// 健康检查接口
api.GET("/health", func(c *gin.Context) {
// 获取插件信息
pluginCount := 0
pluginNames := []string{}
if searchService != nil && searchService.GetPluginManager() != nil {
pluginCount = len(searchService.GetPluginManager().GetPlugins())
plugins := searchService.GetPluginManager().GetPlugins()
pluginCount = len(plugins)
for _, p := range plugins {
pluginNames = append(pluginNames, p.Name())
}
}
// 获取频道信息
channels := config.AppConfig.DefaultChannels
c.JSON(200, gin.H{
"status": "ok",
"plugins_enabled": true,
"plugin_count": pluginCount,
"plugins": pluginNames,
"channels": channels,
})
})
}

View File

@@ -0,0 +1,667 @@
# 异步插件超时动态配置设计方案
## 1. 背景与需求
当前PanSou系统的异步插件响应超时时间ASYNC_RESPONSE_TIMEOUT通过环境变量配置在系统启动时加载所有用户请求共享同一个超时设置。这种方式无法满足不同用户对响应时间的个性化需求有些用户希望更快得到结果即使不完整而有些用户愿意等待更长时间以获取更完整的结果。
因此需要实现一种机制允许通过HTTP请求动态设置异步插件的响应超时时间使不同用户可以根据自己的网络环境和需求调整超时设置。
## 2. 设计理念
### 2.1 核心原则
1. **请求隔离**:每个请求应有独立的超时设置,互不影响
2. **无状态设计**:不修改全局配置状态,确保线程安全
3. **向后兼容**:不破坏现有功能,默认行为保持不变
4. **易于扩展**:设计应便于未来添加更多可动态配置的参数
5. **最小修改**尽量减少对现有代码的修改降低引入bug的风险
### 2.2 技术选型
采用**上下文传递**模式通过请求上下文Context传递超时设置而不是修改全局配置。这种模式有以下优势
1. **线程安全**:每个请求有独立的上下文,不存在并发问题
2. **灵活性高**:可以轻松扩展传递更多参数
3. **符合Go语言习惯**与Go标准库的context包设计理念一致
4. **易于测试**:上下文可以在测试中轻松模拟和替换
## 3. 详细设计
### 3.1 整体架构
```
┌─────────────────────────┐
│ API层 │
│ (解析请求参数,创建上下文) │
└───────────┬─────────────┘
┌───────────▼─────────────┐
│ 服务层 │
│ (传递上下文到插件系统) │
└───────────┬─────────────┘
┌───────────▼─────────────┐
│ 插件系统 │
│ (从上下文获取超时设置) │
└─────────────────────────┘
```
### 3.2 数据结构设计
#### 3.2.1 搜索上下文
```go
// SearchContext 搜索上下文,包含请求特定的配置
type SearchContext struct {
// 异步插件响应超时时间
AsyncResponseTimeout time.Duration
// 其他可能的配置参数(未来扩展)
// PluginTimeout time.Duration
// MaxResults int
// ...
}
// NewSearchContext 创建默认的搜索上下文
func NewSearchContext() *SearchContext {
return &SearchContext{
AsyncResponseTimeout: 0, // 0表示使用系统默认值
}
}
// WithAsyncResponseTimeout 设置异步响应超时时间并返回上下文
func (ctx *SearchContext) WithAsyncResponseTimeout(timeout int) *SearchContext {
if timeout > 0 {
ctx.AsyncResponseTimeout = time.Duration(timeout) * time.Second
}
return ctx
}
// GetAsyncResponseTimeout 获取有效的异步响应超时时间
// 如果上下文中未设置,则返回系统默认值
func (ctx *SearchContext) GetAsyncResponseTimeout() time.Duration {
if ctx.AsyncResponseTimeout > 0 {
return ctx.AsyncResponseTimeout
}
// 使用系统默认值
if config.AppConfig != nil {
return config.AppConfig.AsyncResponseTimeoutDur
}
// 系统配置不可用时的备用值
return 4 * time.Second
}
```
#### 3.2.2 扩展请求模型
```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"` // 结果类型all、results、merge
SourceType string `json:"src"` // 数据来源类型all、tg、plugin
Plugins []string `json:"plugins"` // 指定搜索的插件列表
AsyncTimeout int `json:"async_timeout"` // 异步响应超时时间(秒)
}
```
### 3.3 接口设计
#### 3.3.1 修改插件接口
```go
// SearchPlugin 搜索插件接口
type SearchPlugin interface {
// Name 返回插件名称
Name() string
// Search 执行搜索并返回结果
// 添加ctx参数传递搜索上下文
Search(ctx *SearchContext, keyword string) ([]model.SearchResult, error)
// Priority 返回插件优先级
Priority() int
}
```
#### 3.3.2 修改异步插件基类
```go
// BaseAsyncPlugin 基础异步插件结构
type BaseAsyncPlugin struct {
name string
priority int
client *http.Client
backgroundClient *http.Client
cacheTTL time.Duration
}
// AsyncSearch 异步搜索基础方法
func (p *BaseAsyncPlugin) AsyncSearch(
ctx *SearchContext,
keyword string,
cacheKey string,
searchFunc func(*http.Client, string) ([]model.SearchResult, error),
) ([]model.SearchResult, error) {
// ...现有代码...
// 获取响应超时时间
// 从上下文获取,如果上下文中未设置,则使用系统默认值
responseTimeout := ctx.GetAsyncResponseTimeout()
// ...现有代码...
}
```
#### 3.3.3 修改服务层接口
```go
// SearchService 搜索服务
type SearchService struct{
pluginManager *plugin.PluginManager
}
// Search 执行搜索
// 添加ctx参数传递搜索上下文
func (s *SearchService) Search(
ctx *SearchContext,
keyword string,
channels []string,
concurrency int,
forceRefresh bool,
resultType string,
sourceType string,
plugins []string,
) (model.SearchResponse, error) {
// ...现有代码...
// 如果需要搜索插件
if sourceType == "all" || sourceType == "plugin" {
wg.Add(1)
go func() {
defer wg.Done()
// 传递上下文到searchPlugins方法
pluginResults, pluginErr = s.searchPlugins(ctx, keyword, plugins, forceRefresh, concurrency)
}()
}
// ...现有代码...
}
// searchPlugins 搜索插件
// 添加ctx参数传递搜索上下文
func (s *SearchService) searchPlugins(
ctx *SearchContext,
keyword string,
plugins []string,
forceRefresh bool,
concurrency int,
) ([]model.SearchResult, error) {
// ...现有代码...
// 执行搜索任务时传递上下文
tasks := make([]pool.Task, 0, len(availablePlugins))
for _, p := range availablePlugins {
plugin := p // 创建副本,避免闭包问题
tasks = append(tasks, func() interface{} {
// 传递上下文到插件的Search方法
results, err := plugin.Search(ctx, keyword)
if err != nil {
return nil
}
return results
})
}
// ...现有代码...
}
```
### 3.4 API层实现
```go
// SearchHandler 搜索处理函数
func SearchHandler(c *gin.Context) {
var req model.SearchRequest
var err error
// ...现有代码...
// GET方式处理异步超时参数
asyncTimeout := 0
asyncTimeoutStr := c.Query("async_timeout")
if asyncTimeoutStr != "" && asyncTimeoutStr != " " {
asyncTimeout = util.StringToInt(asyncTimeoutStr)
}
req = model.SearchRequest{
// ...现有字段...
AsyncTimeout: asyncTimeout,
}
// ...现有代码...
// 创建搜索上下文
searchCtx := model.NewSearchContext().WithAsyncResponseTimeout(req.AsyncTimeout)
// 执行搜索,传递上下文
result, err := searchService.Search(
searchCtx,
req.Keyword,
req.Channels,
req.Concurrency,
req.ForceRefresh,
req.ResultType,
req.SourceType,
req.Plugins,
)
// ...现有代码...
}
```
## 4. 具体代码修改
### 4.1 创建搜索上下文model/context.go
```go
package model
import (
"time"
"pansou/config"
)
// SearchContext 搜索上下文,包含请求特定的配置
type SearchContext struct {
// 异步插件响应超时时间
AsyncResponseTimeout time.Duration
}
// NewSearchContext 创建默认的搜索上下文
func NewSearchContext() *SearchContext {
return &SearchContext{
AsyncResponseTimeout: 0, // 0表示使用系统默认值
}
}
// WithAsyncResponseTimeout 设置异步响应超时时间并返回上下文
func (ctx *SearchContext) WithAsyncResponseTimeout(timeout int) *SearchContext {
if timeout > 0 {
ctx.AsyncResponseTimeout = time.Duration(timeout) * time.Second
}
return ctx
}
// GetAsyncResponseTimeout 获取有效的异步响应超时时间
// 如果上下文中未设置,则返回系统默认值
func (ctx *SearchContext) GetAsyncResponseTimeout() time.Duration {
if ctx.AsyncResponseTimeout > 0 {
return ctx.AsyncResponseTimeout
}
// 使用系统默认值
if config.AppConfig != nil {
return config.AppConfig.AsyncResponseTimeoutDur
}
// 系统配置不可用时的备用值
return 4 * time.Second
}
```
### 4.2 修改请求模型model/request.go
```go
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、merge
SourceType string `json:"src"` // 数据来源类型all、tg、plugin
Plugins []string `json:"plugins"` // 指定搜索的插件列表
AsyncTimeout int `json:"async_timeout"` // 异步响应超时时间(秒)
}
```
### 4.3 修改插件接口plugin/plugin.go
```go
package plugin
import (
"pansou/model"
)
// SearchPlugin 搜索插件接口
type SearchPlugin interface {
// Name 返回插件名称
Name() string
// Search 执行搜索并返回结果
// 添加ctx参数传递搜索上下文
Search(ctx *model.SearchContext, keyword string) ([]model.SearchResult, error)
// Priority 返回插件优先级
Priority() int
}
// 兼容层,用于支持旧插件
type legacyPluginAdapter struct {
plugin LegacySearchPlugin
}
// LegacySearchPlugin 旧版插件接口
type LegacySearchPlugin interface {
Name() string
Search(keyword string) ([]model.SearchResult, error)
Priority() int
}
// 创建兼容适配器
func NewLegacyPluginAdapter(plugin LegacySearchPlugin) SearchPlugin {
return &legacyPluginAdapter{plugin: plugin}
}
// 实现新接口
func (a *legacyPluginAdapter) Name() string {
return a.plugin.Name()
}
func (a *legacyPluginAdapter) Search(ctx *model.SearchContext, keyword string) ([]model.SearchResult, error) {
// 忽略上下文,调用旧方法
return a.plugin.Search(keyword)
}
func (a *legacyPluginAdapter) Priority() int {
return a.plugin.Priority()
}
```
### 4.4 修改异步插件基类plugin/baseasyncplugin.go
```go
// AsyncSearch 异步搜索基础方法
func (p *BaseAsyncPlugin) AsyncSearch(
ctx *model.SearchContext,
keyword string,
cacheKey string,
searchFunc func(*http.Client, string) ([]model.SearchResult, error),
) ([]model.SearchResult, error) {
// ...现有代码...
// 获取响应超时时间
// 从上下文获取,如果上下文中未设置,则使用系统默认值
responseTimeout := defaultAsyncResponseTimeout
if ctx != nil {
responseTimeout = ctx.GetAsyncResponseTimeout()
} else 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):
// 响应超时,返回空结果,后台继续处理
// ...现有代码...
}
}
```
### 4.5 修改服务层service/search_service.go
```go
// Search 执行搜索
// 添加ctx参数传递搜索上下文
func (s *SearchService) Search(
ctx *model.SearchContext,
keyword string,
channels []string,
concurrency int,
forceRefresh bool,
resultType string,
sourceType string,
plugins []string,
) (model.SearchResponse, error) {
// 如果未提供上下文,创建默认上下文
if ctx == nil {
ctx = model.NewSearchContext()
}
// ...现有代码...
// 如果需要搜索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" {
wg.Add(1)
go func() {
defer wg.Done()
// 传递上下文到searchPlugins方法
pluginResults, pluginErr = s.searchPlugins(ctx, keyword, plugins, forceRefresh, concurrency)
}()
}
// ...现有代码...
}
// searchPlugins 搜索插件
// 添加ctx参数传递搜索上下文
func (s *SearchService) searchPlugins(
ctx *model.SearchContext,
keyword string,
plugins []string,
forceRefresh bool,
concurrency int,
) ([]model.SearchResult, error) {
// ...现有代码...
// 执行搜索任务时传递上下文
tasks := make([]pool.Task, 0, len(availablePlugins))
for _, p := range availablePlugins {
plugin := p // 创建副本,避免闭包问题
tasks = append(tasks, func() interface{} {
// 传递上下文到插件的Search方法
results, err := plugin.Search(ctx, keyword)
if err != nil {
return nil
}
return results
})
}
// ...现有代码...
}
```
### 4.6 修改API层api/handler.go
```go
// SearchHandler 搜索处理函数
func SearchHandler(c *gin.Context) {
var req model.SearchRequest
var err error
// 根据请求方法不同处理参数
if c.Request.Method == http.MethodGet {
// ...现有代码...
// 处理异步超时参数
asyncTimeout := 0
asyncTimeoutStr := c.Query("async_timeout")
if asyncTimeoutStr != "" && asyncTimeoutStr != " " {
asyncTimeout = util.StringToInt(asyncTimeoutStr)
}
req = model.SearchRequest{
Keyword: keyword,
Channels: channels,
Concurrency: concurrency,
ForceRefresh: forceRefresh,
ResultType: resultType,
SourceType: sourceType,
Plugins: plugins,
AsyncTimeout: asyncTimeout,
}
} else {
// ...现有代码...
}
// ...现有代码...
// 创建搜索上下文
searchCtx := model.NewSearchContext().WithAsyncResponseTimeout(req.AsyncTimeout)
// 执行搜索,传递上下文
result, err := searchService.Search(
searchCtx,
req.Keyword,
req.Channels,
req.Concurrency,
req.ForceRefresh,
req.ResultType,
req.SourceType,
req.Plugins,
)
// ...现有代码...
}
```
### 4.7 修改具体插件实现
以JikePanPlugin为例
```go
// Search 实现搜索接口
func (p *JikePanPlugin) Search(ctx *model.SearchContext, keyword string) ([]model.SearchResult, error) {
// 生成缓存键
cacheKey := generateCacheKey(keyword)
// 使用异步搜索,传递上下文
return p.BaseAsyncPlugin.AsyncSearch(ctx, keyword, cacheKey, p.doSearch)
}
```
## 5. 分阶段实施计划
### 5.1 第一阶段:基础架构改造
**目标**:创建搜索上下文,修改核心接口,但保持向后兼容
**具体任务**
1. 创建`model/context.go`文件,实现`SearchContext`结构体
2. 修改`model/request.go`,添加`AsyncTimeout`字段
3. 修改`plugin/plugin.go`,扩展插件接口并添加兼容层
4. 修改`plugin/baseasyncplugin.go`,支持从上下文获取超时设置
5. 编写单元测试,确保基础架构正常工作
**预计工作量**2人天
### 5.2 第二阶段:服务层改造
**目标**:修改服务层,支持传递和使用搜索上下文
**具体任务**
1. 修改`service/search_service.go`中的`Search`方法,添加上下文参数
2. 修改`searchPlugins`方法,支持传递上下文到插件
3. 确保服务层改动不影响现有功能
4. 编写单元测试,验证服务层正常工作
**预计工作量**1人天
### 5.3 第三阶段API层改造
**目标**修改API层支持从请求中解析超时参数并创建上下文
**具体任务**
1. 修改`api/handler.go`中的`SearchHandler`函数,解析超时参数
2. 创建搜索上下文并传递到服务层
3. 确保API层改动不影响现有功能
4. 编写单元测试验证API层正常工作
**预计工作量**1人天
### 5.4 第四阶段:插件适配
**目标**:适配所有现有插件,支持上下文传递
**具体任务**
1. 修改所有异步插件实现,支持上下文参数
2. 对于同步插件,使用适配器包装
3. 确保所有插件正常工作
4. 编写集成测试,验证整个系统正常工作
**预计工作量**2人天
### 5.5 第五阶段:测试与部署
**目标**:全面测试新功能,确保系统稳定运行
**具体任务**
1. 进行全面的功能测试
2. 进行性能测试,确保改动不影响系统性能
3. 更新API文档添加新参数说明
4. 部署到测试环境进行验证
5. 部署到生产环境
**预计工作量**2人天
## 6. 风险评估与缓解措施
### 6.1 潜在风险
1. **接口变更影响**:插件接口变更可能导致现有插件无法正常工作
- 缓解措施:提供兼容层,确保旧插件仍能正常工作
2. **性能影响**:上下文传递可能增加系统开销
- 缓解措施:确保上下文结构轻量,避免不必要的数据复制
3. **测试覆盖不足**:接口变更涉及面广,可能难以全面测试
- 缓解措施:编写全面的单元测试和集成测试,覆盖各种场景
4. **用户误用**:用户可能设置不合理的超时值
- 缓解措施:添加参数验证,限制超时值在合理范围内
### 6.2 回滚计划
如果部署后发现严重问题,可以采取以下回滚措施:
1. 立即回滚到上一个稳定版本
2. 临时禁用动态超时功能,强制使用系统默认值
3. 修复问题后重新部署
## 7. 总结
本设计方案通过上下文传递模式,实现了异步插件响应超时时间的动态配置功能。这种方式不仅满足了不同用户对响应时间的个性化需求,还保持了良好的线程安全性和扩展性。
通过分阶段实施计划,可以平稳地完成系统改造,同时最大限度地降低对现有功能的影响。完成改造后,系统将具备更高的灵活性和用户体验。
未来,可以基于此架构轻松扩展更多可动态配置的参数,进一步提升系统的可定制性。