diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000..347cc77 Binary files /dev/null and b/.DS_Store differ diff --git a/docs/异步插件超时动态配置设计方案.md b/docs/异步插件超时动态配置设计方案.md deleted file mode 100644 index dd5b4bb..0000000 --- a/docs/异步插件超时动态配置设计方案.md +++ /dev/null @@ -1,667 +0,0 @@ -# 异步插件超时动态配置设计方案 - -## 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. 总结 - -本设计方案通过上下文传递模式,实现了异步插件响应超时时间的动态配置功能。这种方式不仅满足了不同用户对响应时间的个性化需求,还保持了良好的线程安全性和扩展性。 - -通过分阶段实施计划,可以平稳地完成系统改造,同时最大限度地降低对现有功能的影响。完成改造后,系统将具备更高的灵活性和用户体验。 - -未来,可以基于此架构轻松扩展更多可动态配置的参数,进一步提升系统的可定制性。 \ No newline at end of file