mirror of
https://github.com/fish2018/pansou.git
synced 2025-11-25 11:29:30 +08:00
update
This commit is contained in:
804
docs/插件开发指南.md
Normal file
804
docs/插件开发指南.md
Normal file
@@ -0,0 +1,804 @@
|
||||
# PanSou 搜索插件开发指南
|
||||
|
||||
## 目录
|
||||
|
||||
1. [插件系统概述](#插件系统概述)
|
||||
2. [插件接口说明](#插件接口说明)
|
||||
3. [插件开发流程](#插件开发流程)
|
||||
4. [数据结构标准](#数据结构标准)
|
||||
5. [超时控制](#超时控制)
|
||||
6. [最佳实践](#最佳实践)
|
||||
7. [示例插件](#示例插件)
|
||||
8. [常见问题](#常见问题)
|
||||
|
||||
## 插件系统概述
|
||||
|
||||
PanSou 网盘搜索系统采用了灵活的插件架构,允许开发者轻松扩展搜索来源。插件系统具有以下特点:
|
||||
|
||||
- **自动注册机制**:插件通过 init 函数自动注册,无需修改主程序代码
|
||||
- **统一接口**:所有插件实现相同的 SearchPlugin 接口
|
||||
- **双层超时控制**:插件内部使用自定义超时时间,系统外部提供强制超时保障
|
||||
- **并发执行**:插件搜索与频道搜索并发执行,提高整体性能
|
||||
- **结果标准化**:插件返回标准化的搜索结果,便于统一处理
|
||||
|
||||
插件系统的核心是全局插件注册表,它在应用启动时收集所有已注册的插件,并在搜索时并行调用这些插件。
|
||||
|
||||
## 插件接口说明
|
||||
|
||||
每个插件必须实现 `SearchPlugin` 接口,该接口定义如下:
|
||||
|
||||
```go
|
||||
// SearchPlugin 搜索插件接口
|
||||
type SearchPlugin interface {
|
||||
// Name 返回插件名称
|
||||
Name() string
|
||||
|
||||
// Search 执行搜索并返回结果
|
||||
Search(keyword string) ([]model.SearchResult, error)
|
||||
|
||||
// Priority 返回插件优先级(用于控制结果排序)
|
||||
Priority() int
|
||||
}
|
||||
```
|
||||
|
||||
### 接口方法说明
|
||||
|
||||
1. **Name()**
|
||||
- 返回插件的唯一标识名称
|
||||
- 名称应简洁明了,全小写,不含特殊字符
|
||||
- 例如:`pansearch`、`hunhepan`、`jikepan`
|
||||
|
||||
2. **Search(keyword string)**
|
||||
- 执行搜索并返回结果
|
||||
- 参数 `keyword` 是用户输入的搜索关键词
|
||||
- 返回值是搜索结果数组和可能的错误
|
||||
- 实现时应处理超时和错误,确保不会无限阻塞
|
||||
|
||||
3. **Priority()**
|
||||
- 返回插件的优先级,用于控制结果排序
|
||||
- 建议值:1(低)、2(中)、3(高)
|
||||
- 优先级高的插件结果可能会被优先展示
|
||||
|
||||
## 插件开发流程
|
||||
|
||||
### 1. 创建插件包
|
||||
|
||||
在 `pansou/plugin` 目录下创建新的插件包:
|
||||
|
||||
```
|
||||
pansou/
|
||||
└── plugin/
|
||||
└── myplugin/
|
||||
└── myplugin.go
|
||||
```
|
||||
|
||||
### 2. 实现插件结构体
|
||||
|
||||
```go
|
||||
package myplugin
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"pansou/model"
|
||||
"pansou/plugin"
|
||||
)
|
||||
|
||||
// 常量定义
|
||||
const (
|
||||
// 默认超时时间
|
||||
DefaultTimeout = 5 * time.Second
|
||||
)
|
||||
|
||||
// MyPlugin 自定义插件结构体
|
||||
type MyPlugin struct {
|
||||
client *http.Client
|
||||
timeout time.Duration
|
||||
}
|
||||
|
||||
// NewMyPlugin 创建新的插件实例
|
||||
func NewMyPlugin() *MyPlugin {
|
||||
timeout := DefaultTimeout
|
||||
|
||||
return &MyPlugin{
|
||||
client: &http.Client{
|
||||
Timeout: timeout,
|
||||
},
|
||||
timeout: timeout,
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 实现 SearchPlugin 接口
|
||||
|
||||
```go
|
||||
// Name 返回插件名称
|
||||
func (p *MyPlugin) Name() string {
|
||||
return "myplugin"
|
||||
}
|
||||
|
||||
// Priority 返回插件优先级
|
||||
func (p *MyPlugin) Priority() int {
|
||||
return 2 // 中等优先级
|
||||
}
|
||||
|
||||
// Search 执行搜索并返回结果
|
||||
func (p *MyPlugin) Search(keyword string) ([]model.SearchResult, error) {
|
||||
// 实现搜索逻辑
|
||||
// ...
|
||||
|
||||
return results, nil
|
||||
}
|
||||
```
|
||||
|
||||
### 4. 注册插件
|
||||
|
||||
在插件包的 init 函数中注册插件:
|
||||
|
||||
```go
|
||||
// 在init函数中注册插件
|
||||
func init() {
|
||||
plugin.RegisterGlobalPlugin(NewMyPlugin())
|
||||
}
|
||||
```
|
||||
|
||||
### 5. 在主程序中导入插件
|
||||
|
||||
在 `pansou/main.go` 中导入插件包(使用空导入):
|
||||
|
||||
```go
|
||||
import (
|
||||
// 导入插件包以触发init函数
|
||||
_ "pansou/plugin/myplugin"
|
||||
)
|
||||
```
|
||||
|
||||
## 数据结构标准
|
||||
|
||||
### SearchResult 结构体
|
||||
|
||||
插件需要返回 `[]model.SearchResult` 类型的数据:
|
||||
|
||||
```go
|
||||
// SearchResult 表示搜索结果
|
||||
type SearchResult struct {
|
||||
UniqueID string // 唯一标识
|
||||
Title string // 标题
|
||||
Content string // 内容描述
|
||||
Datetime time.Time // 日期时间
|
||||
Links []Link // 链接列表
|
||||
Tags []string // 标签列表
|
||||
}
|
||||
|
||||
// Link 表示网盘链接
|
||||
type Link struct {
|
||||
URL string // 链接地址
|
||||
Type string // 链接类型
|
||||
Password string // 提取码
|
||||
}
|
||||
```
|
||||
|
||||
### 字段说明
|
||||
|
||||
1. **UniqueID**:
|
||||
- 结果的唯一标识,建议格式:`插件名-序号`
|
||||
- 例如:`myplugin-1`、`myplugin-2`
|
||||
|
||||
2. **Title**:
|
||||
- 资源的标题
|
||||
- 应尽可能保留原始标题,不要添加额外信息
|
||||
- 例如:`火影忍者全集高清资源`
|
||||
|
||||
3. **Content**:
|
||||
- 资源的描述内容
|
||||
- 可以包含文件列表、大小、格式等信息
|
||||
- 应清理HTML标签等无关内容
|
||||
|
||||
4. **Datetime**:
|
||||
- 资源的发布时间或更新时间
|
||||
- 如果没有时间信息,使用零值 `time.Time{}`
|
||||
- 不要使用当前时间 `time.Now()`
|
||||
|
||||
5. **Links**:
|
||||
- 资源的链接列表
|
||||
- 每个资源可以有多个不同类型的链接
|
||||
- 每个链接必须包含URL和Type,Password可选
|
||||
|
||||
6. **URL**:
|
||||
- 网盘链接的完整URL
|
||||
- 必须包含协议部分(如http://或https://)
|
||||
- 例如:`https://pan.baidu.com/s/1abcdefg`
|
||||
|
||||
7. **Type**:
|
||||
- 链接类型,必须使用以下标准值之一:
|
||||
- `baidu` - 百度网盘
|
||||
- `aliyun` - 阿里云盘
|
||||
- `xunlei` - 迅雷云盘
|
||||
- `quark` - 夸克网盘
|
||||
- `tianyi` - 天翼云盘
|
||||
- `115` - 115网盘
|
||||
- `weiyun` - 微云
|
||||
- `lanzou` - 蓝奏云
|
||||
- `jianguoyun` - 坚果云
|
||||
- `mobile` - 移动云盘(彩云)
|
||||
- `uc` - UC网盘
|
||||
- `123` - 123网盘
|
||||
- `pikpak` - PikPak网盘
|
||||
- `ed2k` - 电驴链接
|
||||
- `magnet` - 磁力链接
|
||||
- `others` - 其他类型
|
||||
|
||||
8. **Password**:
|
||||
- 提取码或访问密码
|
||||
- 如果没有密码,设置为空字符串
|
||||
|
||||
9. **Tags**:
|
||||
- 资源的标签列表
|
||||
- 可选字段,不是必须提供
|
||||
|
||||
### 具体示例
|
||||
|
||||
下面是几个完整的 `SearchResult` 结构体示例,展示了不同情况下的数据填充方式:
|
||||
|
||||
#### 示例1:带有百度网盘链接的电影资源
|
||||
|
||||
```go
|
||||
// 创建一个带有百度网盘链接的电影资源搜索结果
|
||||
movieResult := model.SearchResult{
|
||||
UniqueID: "myplugin-1",
|
||||
Title: "速度与激情10 4K蓝光原盘",
|
||||
Content: "文件列表:\n- 速度与激情10.mp4 (25.6GB)\n- 花絮.mp4 (1.2GB)\n- 字幕.zip (15MB)",
|
||||
Datetime: time.Date(2023, 8, 15, 10, 30, 0, 0, time.Local), // 2023-08-15 10:30:00
|
||||
Links: []model.Link{
|
||||
{
|
||||
URL: "https://pan.baidu.com/s/1abcdefghijklmn",
|
||||
Type: "baidu",
|
||||
Password: "a1b2",
|
||||
},
|
||||
},
|
||||
Tags: []string{"电影", "动作", "4K"},
|
||||
}
|
||||
```
|
||||
|
||||
#### 示例2:带有多个网盘链接的软件资源
|
||||
|
||||
```go
|
||||
// 创建一个带有多个网盘链接的软件资源搜索结果
|
||||
softwareResult := model.SearchResult{
|
||||
UniqueID: "myplugin-2",
|
||||
Title: "Photoshop 2023 完整破解版 Win+Mac",
|
||||
Content: "Adobe Photoshop 2023 完整破解版,支持Windows和Mac系统,内含安装教程和注册机。",
|
||||
Datetime: time.Date(2023, 6, 20, 15, 45, 0, 0, time.Local), // 2023-06-20 15:45:00
|
||||
Links: []model.Link{
|
||||
{
|
||||
URL: "https://pan.baidu.com/s/1opqrstuvwxyz",
|
||||
Type: "baidu",
|
||||
Password: "c3d4",
|
||||
},
|
||||
{
|
||||
URL: "https://www.aliyundrive.com/s/abcdefghijk",
|
||||
Type: "aliyun",
|
||||
Password: "", // 阿里云盘无提取码
|
||||
},
|
||||
{
|
||||
URL: "https://pan.xunlei.com/s/12345678",
|
||||
Type: "xunlei",
|
||||
Password: "xunl",
|
||||
},
|
||||
},
|
||||
Tags: []string{"软件", "设计", "Adobe"},
|
||||
}
|
||||
```
|
||||
|
||||
#### 示例3:带有磁力链接的资源
|
||||
|
||||
```go
|
||||
// 创建一个带有磁力链接的资源搜索结果
|
||||
torrentResult := model.SearchResult{
|
||||
UniqueID: "myplugin-3",
|
||||
Title: "权力的游戏 第一季 1080P 中英双字",
|
||||
Content: "权力的游戏第一季全10集,1080P高清版本,内封中英双字幕。",
|
||||
Datetime: time.Date(2022, 12, 5, 8, 0, 0, 0, time.Local), // 2022-12-05 08:00:00
|
||||
Links: []model.Link{
|
||||
{
|
||||
URL: "magnet:?xt=urn:btih:1a2b3c4d5e6f7g8h9i0j1k2l3m4n5o6p7q8r9s0t",
|
||||
Type: "magnet",
|
||||
Password: "", // 磁力链接没有密码
|
||||
},
|
||||
},
|
||||
Tags: []string{"美剧", "奇幻", "1080P"},
|
||||
}
|
||||
```
|
||||
|
||||
#### 示例4:没有时间信息的资源
|
||||
|
||||
```go
|
||||
// 创建一个没有时间信息的资源搜索结果
|
||||
noTimeResult := model.SearchResult{
|
||||
UniqueID: "myplugin-4",
|
||||
Title: "中国历史文化名人传记合集",
|
||||
Content: "包含100位中国历史文化名人的详细传记,PDF格式。",
|
||||
Datetime: time.Time{}, // 使用零值表示没有时间信息
|
||||
Links: []model.Link{
|
||||
{
|
||||
URL: "https://pan.quark.cn/s/12345abcde",
|
||||
Type: "quark",
|
||||
Password: "qwer",
|
||||
},
|
||||
},
|
||||
Tags: []string{"电子书", "历史", "传记"},
|
||||
}
|
||||
```
|
||||
|
||||
#### 示例5:多种文件格式的教程资源
|
||||
|
||||
```go
|
||||
// 创建一个包含多种文件格式的教程资源搜索结果
|
||||
tutorialResult := model.SearchResult{
|
||||
UniqueID: "myplugin-5",
|
||||
Title: "Python数据分析实战教程 2023最新版",
|
||||
Content: "包含视频教程、源代码、PPT讲义和练习题。适合Python初学者和有一定基础的开发者。",
|
||||
Datetime: time.Date(2023, 9, 1, 12, 0, 0, 0, time.Local), // 2023-09-01 12:00:00
|
||||
Links: []model.Link{
|
||||
{
|
||||
URL: "https://cloud.189.cn/t/abcdefg123456",
|
||||
Type: "tianyi",
|
||||
Password: "189t",
|
||||
},
|
||||
{
|
||||
URL: "https://caiyun.139.com/m/i?abcdefghijk",
|
||||
Type: "mobile",
|
||||
Password: "139c",
|
||||
},
|
||||
},
|
||||
Tags: []string{"教程", "Python", "数据分析"},
|
||||
}
|
||||
```
|
||||
|
||||
### 返回结果示例
|
||||
|
||||
插件的 `Search` 方法应返回一个 `[]model.SearchResult` 切片,包含所有搜索结果:
|
||||
|
||||
```go
|
||||
// Search 执行搜索并返回结果
|
||||
func (p *MyPlugin) Search(keyword string) ([]model.SearchResult, error) {
|
||||
// ... 执行搜索逻辑 ...
|
||||
|
||||
// 创建结果切片
|
||||
results := []model.SearchResult{
|
||||
movieResult,
|
||||
softwareResult,
|
||||
torrentResult,
|
||||
noTimeResult,
|
||||
tutorialResult,
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
```
|
||||
|
||||
### 注意事项
|
||||
|
||||
1. **链接类型映射**:
|
||||
如果源站点使用的链接类型名称与标准不同,需要进行映射,例如:
|
||||
|
||||
```go
|
||||
func mapLinkType(sourceType string) string {
|
||||
switch strings.ToLower(sourceType) {
|
||||
case "bd", "bdy", "baidu_pan":
|
||||
return "baidu"
|
||||
case "al", "aly", "aliyundrive":
|
||||
return "aliyun"
|
||||
case "ty", "tianyi_pan":
|
||||
return "tianyi"
|
||||
// ... 其他映射
|
||||
default:
|
||||
return "others"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
2. **URL格式化**:
|
||||
确保URL格式正确,特别是对于特殊链接类型:
|
||||
|
||||
```go
|
||||
// 确保百度网盘链接格式正确
|
||||
if !strings.HasPrefix(url, "https://") && !strings.HasPrefix(url, "http://") {
|
||||
url = "https://" + url
|
||||
}
|
||||
|
||||
// 确保磁力链接格式正确
|
||||
if strings.HasPrefix(url, "magnet:") && !strings.HasPrefix(url, "magnet:?xt=urn:btih:") {
|
||||
// 格式不正确,尝试修复或跳过
|
||||
}
|
||||
```
|
||||
|
||||
3. **密码处理**:
|
||||
对于不同网盘的密码格式可能有所不同,需要适当处理:
|
||||
|
||||
```go
|
||||
// 百度网盘密码通常为4位
|
||||
if linkType == "baidu" && len(password) > 4 {
|
||||
password = password[:4]
|
||||
}
|
||||
|
||||
// 有些网盘可能在URL中包含密码参数
|
||||
if linkType == "aliyun" && password == "" {
|
||||
// 尝试从URL中提取密码
|
||||
if pwdIndex := strings.Index(url, "password="); pwdIndex != -1 {
|
||||
password = url[pwdIndex+9:]
|
||||
if endIndex := strings.Index(password, "&"); endIndex != -1 {
|
||||
password = password[:endIndex]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 超时控制
|
||||
|
||||
PanSou 采用双层超时控制机制,确保搜索请求能够在合理的时间内完成:
|
||||
|
||||
### 插件内部超时控制
|
||||
|
||||
每个插件应定义并使用自己的默认超时时间:
|
||||
|
||||
```go
|
||||
const (
|
||||
// 默认超时时间
|
||||
DefaultTimeout = 5 * time.Second
|
||||
)
|
||||
|
||||
// NewMyPlugin 创建新的插件实例
|
||||
func NewMyPlugin() *MyPlugin {
|
||||
timeout := DefaultTimeout
|
||||
|
||||
return &MyPlugin{
|
||||
client: &http.Client{
|
||||
Timeout: timeout,
|
||||
},
|
||||
timeout: timeout,
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
插件应根据自身特点设置合适的超时时间:
|
||||
- 需要并发请求多个页面的插件可能设置较短的单次请求超时
|
||||
- 需要处理大量数据的插件可能设置较长的超时
|
||||
|
||||
### 系统外部超时控制
|
||||
|
||||
系统使用 `ExecuteBatchWithTimeout` 函数对所有插件任务进行统一的超时控制。即使插件内部没有正确处理超时,系统也能确保整体搜索在合理时间内完成。
|
||||
|
||||
超时时间通过环境变量 `PLUGIN_TIMEOUT` 配置,默认为 30 秒。
|
||||
|
||||
## 最佳实践
|
||||
|
||||
### 1. 错误处理
|
||||
|
||||
- 妥善处理HTTP请求错误
|
||||
- 解析失败时返回有意义的错误信息
|
||||
- 单个结果解析失败不应影响整体搜索
|
||||
|
||||
```go
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("请求失败: %w", err)
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 并发控制
|
||||
|
||||
- 如果需要发起多个请求,使用并发控制
|
||||
- 使用信号量或工作池限制并发数
|
||||
- 确保所有goroutine都能正确退出
|
||||
|
||||
```go
|
||||
// 创建信号量限制并发数
|
||||
semaphore := make(chan struct{}, maxConcurrent)
|
||||
|
||||
// 使用信号量
|
||||
semaphore <- struct{}{}
|
||||
defer func() { <-semaphore }()
|
||||
```
|
||||
|
||||
### 3. 结果去重
|
||||
|
||||
- 在返回结果前进行初步去重
|
||||
- 使用map存储唯一标识符
|
||||
- 系统会在合并所有插件结果时进行最终去重
|
||||
|
||||
```go
|
||||
// 使用map进行去重
|
||||
uniqueMap := make(map[string]Item)
|
||||
|
||||
// 将去重后的结果转换为切片
|
||||
results := make([]Item, 0, len(uniqueMap))
|
||||
for _, item := range uniqueMap {
|
||||
results = append(results, item)
|
||||
}
|
||||
```
|
||||
|
||||
### 4. 清理HTML标签
|
||||
|
||||
- 清理标题和内容中的HTML标签
|
||||
- 移除多余的空格和换行符
|
||||
- 保留有用的格式信息
|
||||
|
||||
```go
|
||||
func cleanHTML(html string) string {
|
||||
// 替换常见HTML标签
|
||||
replacements := map[string]string{
|
||||
"<em>": "",
|
||||
"</em>": "",
|
||||
"<b>": "",
|
||||
"</b>": "",
|
||||
}
|
||||
|
||||
result := html
|
||||
for tag, replacement := range replacements {
|
||||
result = strings.Replace(result, tag, replacement, -1)
|
||||
}
|
||||
|
||||
return strings.TrimSpace(result)
|
||||
}
|
||||
```
|
||||
|
||||
### 5. 时间解析
|
||||
|
||||
- 正确解析资源的发布时间
|
||||
- 如果无法获取时间,使用零值
|
||||
- 不要使用当前时间代替缺失的时间
|
||||
|
||||
```go
|
||||
// 尝试解析时间
|
||||
var datetime time.Time
|
||||
if item.Time != "" {
|
||||
parsedTime, err := time.Parse("2006-01-02 15:04:05", item.Time)
|
||||
if err == nil {
|
||||
datetime = parsedTime
|
||||
}
|
||||
}
|
||||
|
||||
// 如果解析失败,使用零值
|
||||
if datetime.IsZero() {
|
||||
datetime = time.Time{}
|
||||
}
|
||||
```
|
||||
|
||||
## 示例插件
|
||||
|
||||
以下是一个完整的示例插件实现:
|
||||
|
||||
```go
|
||||
package exampleplugin
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"pansou/model"
|
||||
"pansou/plugin"
|
||||
)
|
||||
|
||||
// 在init函数中注册插件
|
||||
func init() {
|
||||
plugin.RegisterGlobalPlugin(NewExamplePlugin())
|
||||
}
|
||||
|
||||
const (
|
||||
// API端点
|
||||
ApiURL = "https://example.com/api/search"
|
||||
|
||||
// 默认超时时间
|
||||
DefaultTimeout = 5 * time.Second
|
||||
)
|
||||
|
||||
// ExamplePlugin 示例插件
|
||||
type ExamplePlugin struct {
|
||||
client *http.Client
|
||||
timeout time.Duration
|
||||
}
|
||||
|
||||
// NewExamplePlugin 创建新的示例插件
|
||||
func NewExamplePlugin() *ExamplePlugin {
|
||||
timeout := DefaultTimeout
|
||||
|
||||
return &ExamplePlugin{
|
||||
client: &http.Client{
|
||||
Timeout: timeout,
|
||||
},
|
||||
timeout: timeout,
|
||||
}
|
||||
}
|
||||
|
||||
// Name 返回插件名称
|
||||
func (p *ExamplePlugin) Name() string {
|
||||
return "exampleplugin"
|
||||
}
|
||||
|
||||
// Priority 返回插件优先级
|
||||
func (p *ExamplePlugin) Priority() int {
|
||||
return 2 // 中等优先级
|
||||
}
|
||||
|
||||
// Search 执行搜索并返回结果
|
||||
func (p *ExamplePlugin) Search(keyword string) ([]model.SearchResult, error) {
|
||||
// 构建请求URL
|
||||
reqURL := fmt.Sprintf("%s?q=%s", ApiURL, url.QueryEscape(keyword))
|
||||
|
||||
// 发送请求
|
||||
req, err := http.NewRequest("GET", reqURL, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("创建请求失败: %w", err)
|
||||
}
|
||||
|
||||
req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36")
|
||||
|
||||
// 发送请求
|
||||
resp, err := p.client.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("请求失败: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// 读取响应体
|
||||
respBody, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("读取响应失败: %w", err)
|
||||
}
|
||||
|
||||
// 解析响应
|
||||
var apiResp ApiResponse
|
||||
if err := json.Unmarshal(respBody, &apiResp); err != nil {
|
||||
return nil, fmt.Errorf("解析响应失败: %w", err)
|
||||
}
|
||||
|
||||
// 转换为标准格式
|
||||
results := make([]model.SearchResult, 0, len(apiResp.Items))
|
||||
|
||||
for i, item := range apiResp.Items {
|
||||
// 解析时间
|
||||
var datetime time.Time
|
||||
if item.Time != "" {
|
||||
parsedTime, err := time.Parse("2006-01-02 15:04:05", item.Time)
|
||||
if err == nil {
|
||||
datetime = parsedTime
|
||||
}
|
||||
}
|
||||
|
||||
// 如果解析失败,使用零值
|
||||
if datetime.IsZero() {
|
||||
datetime = time.Time{}
|
||||
}
|
||||
|
||||
// 创建链接
|
||||
link := model.Link{
|
||||
URL: item.URL,
|
||||
Type: p.determineLinkType(item.URL),
|
||||
Password: item.Password,
|
||||
}
|
||||
|
||||
// 创建唯一ID
|
||||
uniqueID := fmt.Sprintf("exampleplugin-%d", i)
|
||||
|
||||
// 创建搜索结果
|
||||
result := model.SearchResult{
|
||||
UniqueID: uniqueID,
|
||||
Title: cleanHTML(item.Title),
|
||||
Content: cleanHTML(item.Description),
|
||||
Datetime: datetime,
|
||||
Links: []model.Link{link},
|
||||
}
|
||||
|
||||
results = append(results, result)
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
// determineLinkType 根据URL确定链接类型
|
||||
func (p *ExamplePlugin) determineLinkType(url string) string {
|
||||
lowerURL := strings.ToLower(url)
|
||||
|
||||
switch {
|
||||
case strings.Contains(lowerURL, "pan.baidu.com"):
|
||||
return "baidu"
|
||||
case strings.Contains(lowerURL, "alipan.com") || strings.Contains(lowerURL, "aliyundrive.com"):
|
||||
return "aliyun"
|
||||
case strings.Contains(lowerURL, "pan.xunlei.com"):
|
||||
return "xunlei"
|
||||
// ... 其他类型判断
|
||||
default:
|
||||
return "others"
|
||||
}
|
||||
}
|
||||
|
||||
// cleanHTML 清理HTML标签
|
||||
func cleanHTML(html string) string {
|
||||
// 替换常见HTML标签
|
||||
replacements := map[string]string{
|
||||
"<em>": "",
|
||||
"</em>": "",
|
||||
"<b>": "",
|
||||
"</b>": "",
|
||||
}
|
||||
|
||||
result := html
|
||||
for tag, replacement := range replacements {
|
||||
result = strings.Replace(result, tag, replacement, -1)
|
||||
}
|
||||
|
||||
return strings.TrimSpace(result)
|
||||
}
|
||||
|
||||
// ApiResponse API响应结构
|
||||
type ApiResponse struct {
|
||||
Items []ApiItem `json:"items"`
|
||||
Total int `json:"total"`
|
||||
}
|
||||
|
||||
// ApiItem API响应中的单个结果项
|
||||
type ApiItem struct {
|
||||
Title string `json:"title"`
|
||||
Description string `json:"description"`
|
||||
URL string `json:"url"`
|
||||
Password string `json:"password"`
|
||||
Time string `json:"time"`
|
||||
}
|
||||
```
|
||||
|
||||
## 常见问题
|
||||
|
||||
### 1. 插件注册失败
|
||||
|
||||
**问题**:插件未被系统识别和加载
|
||||
|
||||
**解决方案**:
|
||||
- 确保在 `init()` 函数中调用了 `plugin.RegisterGlobalPlugin()`
|
||||
- 确保在 `main.go` 中导入了插件包(使用空导入)
|
||||
- 检查插件名称是否为空或重复
|
||||
|
||||
### 2. 搜索超时
|
||||
|
||||
**问题**:插件搜索经常超时
|
||||
|
||||
**解决方案**:
|
||||
- 调整插件的默认超时时间
|
||||
- 使用并发请求减少总体响应时间
|
||||
- 实现请求重试机制
|
||||
- 优化请求逻辑,减少不必要的请求
|
||||
|
||||
### 3. 结果格式错误
|
||||
|
||||
**问题**:插件返回的结果格式不正确
|
||||
|
||||
**解决方案**:
|
||||
- 严格按照数据结构标准构造返回值
|
||||
- 确保链接类型使用标准值
|
||||
- 正确处理时间格式
|
||||
- 清理HTML标签和特殊字符
|
||||
|
||||
### 4. 内存泄漏
|
||||
|
||||
**问题**:插件导致内存使用量持续增长
|
||||
|
||||
**解决方案**:
|
||||
- 确保所有goroutine都能正确退出
|
||||
- 关闭HTTP响应体
|
||||
- 避免无限循环
|
||||
- 限制结果集大小
|
||||
|
||||
### 5. 错误处理不当
|
||||
|
||||
**问题**:插件错误影响了整个系统
|
||||
|
||||
**解决方案**:
|
||||
- 捕获并记录所有可能的错误
|
||||
- 使用超时控制避免长时间阻塞
|
||||
- 在返回错误前进行必要的资源清理
|
||||
- 对于非致命错误,返回部分结果而不是完全失败
|
||||
Reference in New Issue
Block a user