diff --git a/docs/插件开发指南.md b/docs/插件开发指南.md
index 64cd730..1d363a0 100644
--- a/docs/插件开发指南.md
+++ b/docs/插件开发指南.md
@@ -309,7 +309,111 @@ func (p *MyPlugin) doRequestWithRetry(req *http.Request, client *http.Client) (*
}
```
-### 3. 链接转换
+### 3. 链接转换与 work_title 字段
+
+#### Link 结构定义
+
+```go
+type Link struct {
+ Type string `json:"type"` // 网盘类型
+ URL string `json:"url"` // 链接地址
+ Password string `json:"password"` // 提取码/密码
+ Datetime time.Time `json:"datetime,omitempty"` // 链接更新时间(可选)
+ WorkTitle string `json:"work_title,omitempty"` // 作品标题(重要:用于区分同一消息中多个作品的链接)
+}
+```
+
+#### work_title 字段详解
+
+**字段作用**:
+- 用于区分**同一条消息/结果中包含的多个不同作品**的链接
+- 特别适用于论坛帖子、TG频道消息等一次性发布多部影视资源的场景
+
+**使用场景示例**:
+
+```
+📺 TG频道消息示例:
+【今日更新】多部热门剧集
+1. 凡人修仙传 第30集
+ 夸克:https://pan.quark.cn/s/abc123
+2. 唐朝诡事录 第20集
+ 夸克:https://pan.quark.cn/s/def456
+3. 庆余年2 全集
+ 百度:https://pan.baidu.com/s/xyz789?pwd=abcd
+```
+
+**不使用 work_title 的问题**:
+- 所有链接的标题都是 "【今日更新】多部热门剧集"
+- 用户无法区分哪个链接对应哪部剧集
+
+**使用 work_title 后的效果**:
+```go
+links := []model.Link{
+ {
+ Type: "quark",
+ URL: "https://pan.quark.cn/s/abc123",
+ WorkTitle: "凡人修仙传 第30集", // 独立作品标题
+ },
+ {
+ Type: "quark",
+ URL: "https://pan.quark.cn/s/def456",
+ WorkTitle: "唐朝诡事录 第20集", // 独立作品标题
+ },
+ {
+ Type: "baidu",
+ URL: "https://pan.baidu.com/s/xyz789?pwd=abcd",
+ Password: "abcd",
+ WorkTitle: "庆余年2 全集", // 独立作品标题
+ },
+}
+```
+
+**PanSou系统的智能处理**:
+
+PanSou 会根据消息中的链接数量自动决定是否提取 work_title:
+
+1. **链接数量 ≤ 4**:所有链接使用相同的 work_title(即消息标题)
+ ```go
+ // 示例:一条消息只包含同一部剧的不同网盘链接
+ // 消息标题:"凡人修仙传 第30集"
+ // 链接1(夸克)、链接2(百度) → work_title 都是 "凡人修仙传 第30集"
+ ```
+
+2. **链接数量 > 4**:系统智能识别每个链接对应的作品标题
+ ```go
+ // 示例:一条消息包含5个不同作品的链接
+ // 系统会分析消息文本,为每个链接提取独立的 work_title
+ ```
+
+**插件实现 work_title 的两种方式**:
+
+**方式1: 依赖系统自动提取**(适用于TG频道、论坛等)
+```go
+// 直接返回链接,系统会自动调用 extractWorkTitlesForLinks 进行处理
+links := []model.Link{
+ {Type: "quark", URL: "https://pan.quark.cn/s/abc123"},
+ {Type: "baidu", URL: "https://pan.baidu.com/s/xyz789"},
+}
+// PanSou会根据消息文本自动为每个链接提取work_title
+```
+
+**方式2: 插件手动设置**(适用于API插件、磁力搜索等)
+```go
+// 插件直接设置 work_title(如feikuai、thepiratebay等)
+links := []model.Link{
+ {
+ Type: "magnet",
+ URL: magnetURL,
+ WorkTitle: buildWorkTitle(keyword, fileName), // 插件自己构建
+ Datetime: publishedTime,
+ },
+}
+```
+
+**插件开发建议**:
+- **网盘API插件**: 如果API直接返回单一作品,可以不设置 work_title(留空)
+- **磁力搜索插件**: 建议设置 work_title,特别是文件名不含中文时需要拼接关键词
+- **爬虫插件**: 如果能从页面提取每个链接的独立标题,建议设置 work_title
#### 支持的网盘类型
@@ -400,7 +504,283 @@ func convertAPILinks(apiLinks []APILink) []model.Link {
## 高级特性
-### 1. Service层过滤控制详解
+### 1. 插件Web路由注册(自定义HTTP接口)
+
+#### 概述
+
+PanSou 支持插件注册自定义的 HTTP 路由,用于实现插件专属的管理页面、配置接口或其他Web功能。
+
+**典型应用场景**:
+- 插件配置管理界面(如 QQPD 的用户登录和频道管理)
+- 插件数据查询接口
+- 插件状态监控页面
+- OAuth回调接口
+
+#### 接口定义
+
+```go
+// PluginWithWebHandler 支持Web路由的插件接口
+// 插件可以选择实现此接口来注册自定义的HTTP路由
+type PluginWithWebHandler interface {
+ AsyncSearchPlugin // 继承搜索插件接口
+
+ // RegisterWebRoutes 注册Web路由
+ // router: gin的路由组,插件可以在此注册自己的路由
+ RegisterWebRoutes(router *gin.RouterGroup)
+}
+```
+
+#### 实现步骤
+
+**步骤1: 插件结构实现接口**
+
+```go
+package myplugin
+
+import (
+ "github.com/gin-gonic/gin"
+ "pansou/plugin"
+ "pansou/model"
+)
+
+type MyPlugin struct {
+ *plugin.BaseAsyncPlugin
+ // ... 其他字段
+}
+
+// 确保插件实现了 PluginWithWebHandler 接口
+var _ plugin.PluginWithWebHandler = (*MyPlugin)(nil)
+```
+
+**步骤2: 实现 RegisterWebRoutes 方法**
+
+```go
+// RegisterWebRoutes 注册Web路由
+func (p *MyPlugin) RegisterWebRoutes(router *gin.RouterGroup) {
+ // 创建插件专属的路由组
+ myGroup := router.Group("/myplugin")
+
+ // 注册GET路由
+ myGroup.GET("/status", p.handleGetStatus)
+
+ // 注册POST路由
+ myGroup.POST("/config", p.handleUpdateConfig)
+
+ // 支持动态路径参数
+ myGroup.GET("/:id", p.handleGetByID)
+ myGroup.POST("/:id/action", p.handleAction)
+}
+```
+
+**步骤3: 实现路由处理函数**
+
+```go
+// handleGetStatus 获取插件状态
+func (p *MyPlugin) handleGetStatus(c *gin.Context) {
+ c.JSON(200, gin.H{
+ "status": "ok",
+ "plugin": p.Name(),
+ "version": "1.0.0",
+ })
+}
+
+// handleUpdateConfig 更新插件配置
+func (p *MyPlugin) handleUpdateConfig(c *gin.Context) {
+ var config map[string]interface{}
+
+ if err := c.BindJSON(&config); err != nil {
+ c.JSON(400, gin.H{"error": "Invalid JSON"})
+ return
+ }
+
+ // 处理配置更新逻辑
+ // ...
+
+ c.JSON(200, gin.H{
+ "success": true,
+ "message": "配置已更新",
+ })
+}
+
+// handleGetByID 根据ID获取数据
+func (p *MyPlugin) handleGetByID(c *gin.Context) {
+ id := c.Param("id")
+
+ // 根据ID查询数据
+ // ...
+
+ c.JSON(200, gin.H{
+ "id": id,
+ "data": "...",
+ })
+}
+```
+
+#### 实际案例: QQPD 插件
+
+QQPD 插件实现了完整的用户管理和频道配置功能:
+
+```go
+// RegisterWebRoutes 注册Web路由
+func (p *QQPDPlugin) RegisterWebRoutes(router *gin.RouterGroup) {
+ qqpd := router.Group("/qqpd")
+
+ // GET /:param - 显示管理页面(HTML)
+ qqpd.GET("/:param", p.handleManagePage)
+
+ // POST /:param - 处理管理操作(JSON API)
+ qqpd.POST("/:param", p.handleManagePagePOST)
+}
+
+// handleManagePage 渲染管理页面
+func (p *QQPDPlugin) handleManagePage(c *gin.Context) {
+ param := c.Param("param")
+
+ // 生成用户专属的管理页面
+ html := strings.ReplaceAll(HTMLTemplate, "HASH_PLACEHOLDER", param)
+
+ c.Header("Content-Type", "text/html; charset=utf-8")
+ c.String(200, html)
+}
+
+// handleManagePagePOST 处理管理操作
+func (p *QQPDPlugin) handleManagePagePOST(c *gin.Context) {
+ param := c.Param("param")
+
+ var req struct {
+ Action string `json:"action"`
+ Channels []string `json:"channels,omitempty"`
+ Keyword string `json:"keyword,omitempty"`
+ }
+
+ if err := c.BindJSON(&req); err != nil {
+ respondError(c, "无效的请求格式")
+ return
+ }
+
+ // 根据不同的 action 执行不同的操作
+ switch req.Action {
+ case "get_status":
+ p.handleGetStatus(c, param)
+ case "set_channels":
+ p.handleSetChannels(c, param, req.Channels)
+ case "test_search":
+ p.handleTestSearch(c, param, req.Keyword)
+ case "logout":
+ p.handleLogout(c, param)
+ default:
+ respondError(c, "未知的操作")
+ }
+}
+```
+
+#### 实际案例: Gying 插件
+
+```go
+// RegisterWebRoutes 注册Web路由
+func (p *GyingPlugin) RegisterWebRoutes(router *gin.RouterGroup) {
+ gying := router.Group("/gying")
+ gying.GET("/:param", p.handleManagePage)
+ gying.POST("/:param", p.handleManagePagePOST)
+}
+```
+
+#### 路由访问示例
+
+插件注册的路由可以通过以下方式访问:
+
+```bash
+# QQPD 插件管理页面
+GET http://localhost:8888/qqpd/user123
+
+# QQPD 插件配置接口
+POST http://localhost:8888/qqpd/user123
+Content-Type: application/json
+{
+ "action": "set_channels",
+ "channels": ["pd97631607", "kuake12345"]
+}
+
+# 自定义插件接口
+GET http://localhost:8888/myplugin/status
+POST http://localhost:8888/myplugin/config
+GET http://localhost:8888/myplugin/resource123
+```
+
+#### 系统集成
+
+PanSou 在启动时会自动扫描并注册所有实现了 `PluginWithWebHandler` 接口的插件路由:
+
+```go
+// api/router.go 中的自动注册逻辑
+func SetupRouter(searchService *service.SearchService) *gin.Engine {
+ r := gin.Default()
+
+ // ... 其他路由配置 ...
+
+ // 注册插件的Web路由(如果插件实现了PluginWithWebHandler接口)
+ allPlugins := plugin.GetRegisteredPlugins()
+ for _, p := range allPlugins {
+ if webPlugin, ok := p.(plugin.PluginWithWebHandler); ok {
+ webPlugin.RegisterWebRoutes(r.Group(""))
+ }
+ }
+
+ return r
+}
+```
+
+#### 开发建议
+
+1. **路由命名规范**: 使用插件名作为路由前缀,避免与其他插件冲突
+ ```go
+ // ✅ 推荐
+ router.Group("/myplugin")
+
+ // ❌ 避免
+ router.Group("/config") // 可能与其他插件冲突
+ ```
+
+2. **安全考虑**:
+ - 对敏感操作进行身份验证
+ - 验证用户输入,防止注入攻击
+ - 使用哈希或加密保护敏感参数
+
+3. **错误处理**: 统一错误响应格式
+ ```go
+ func respondError(c *gin.Context, message string) {
+ c.JSON(400, gin.H{
+ "success": false,
+ "message": message,
+ })
+ }
+
+ func respondSuccess(c *gin.Context, message string, data interface{}) {
+ c.JSON(200, gin.H{
+ "success": true,
+ "message": message,
+ "data": data,
+ })
+ }
+ ```
+
+4. **HTML模板**: 可以内嵌HTML模板提供管理界面
+ ```go
+ const HTMLTemplate = `
+
+
+ 插件管理
+
+
+ {{ .PluginName }} 管理界面
+
+
+ `
+ ```
+
+5. **可选实现**: Web路由是**可选功能**,只有需要自定义HTTP接口的插件才需要实现
+
+### 2. Service层过滤控制详解
#### 构造函数选择
diff --git a/main.go b/main.go
index 0b5134c..df4cfb3 100644
--- a/main.go
+++ b/main.go
@@ -87,6 +87,7 @@ import (
_ "pansou/plugin/ash"
_ "pansou/plugin/qqpd"
_ "pansou/plugin/weibo"
+ _ "pansou/plugin/feikuai"
)
// 全局缓存写入管理器
diff --git a/plugin/feikuai/feikuai.go b/plugin/feikuai/feikuai.go
new file mode 100644
index 0000000..f006d66
--- /dev/null
+++ b/plugin/feikuai/feikuai.go
@@ -0,0 +1,422 @@
+package feikuai
+
+import (
+ "context"
+ "fmt"
+ "io"
+ "net/http"
+ "net/url"
+ "regexp"
+ "strings"
+ "time"
+
+ "pansou/model"
+ "pansou/plugin"
+ "pansou/util/json"
+)
+
+const (
+ // API URL格式
+ SearchAPIURL = "https://feikuai.tv/t_search/bm_search.php?kw=%s"
+
+ // 默认超时时间
+ DefaultTimeout = 15 * time.Second
+
+ // HTTP连接池配置
+ MaxIdleConns = 100
+ MaxIdleConnsPerHost = 30
+ MaxConnsPerHost = 50
+ IdleConnTimeout = 90 * time.Second
+)
+
+// 预编译正则表达式
+var (
+ // 文件扩展名正则
+ fileExtRegex = regexp.MustCompile(`\.(mkv|mp4|avi|rmvb|wmv|flv|mov|ts|m2ts|iso)$`)
+
+ // 文件大小信息正则
+ fileSizeRegex = regexp.MustCompile(`\s*·\s*[\d.]+\s*[KMGT]B\s*$`)
+
+ // 日期时间提取正则
+ dateTimeRegex = regexp.MustCompile(`@[^-]+-(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})`)
+)
+
+// FeikuaiPlugin Feikuai磁力搜索插件
+type FeikuaiPlugin struct {
+ *plugin.BaseAsyncPlugin
+ optimizedClient *http.Client
+}
+
+// createOptimizedHTTPClient 创建优化的HTTP客户端
+func createOptimizedHTTPClient() *http.Client {
+ transport := &http.Transport{
+ MaxIdleConns: MaxIdleConns,
+ MaxIdleConnsPerHost: MaxIdleConnsPerHost,
+ MaxConnsPerHost: MaxConnsPerHost,
+ IdleConnTimeout: IdleConnTimeout,
+ DisableKeepAlives: false,
+ }
+
+ return &http.Client{
+ Transport: transport,
+ Timeout: DefaultTimeout,
+ }
+}
+
+// NewFeikuaiPlugin 创建新的Feikuai插件
+func NewFeikuaiPlugin() *FeikuaiPlugin {
+ return &FeikuaiPlugin{
+ BaseAsyncPlugin: plugin.NewBaseAsyncPluginWithFilter("feikuai", 3, true), // 跳过Service层过滤
+ optimizedClient: createOptimizedHTTPClient(),
+ }
+}
+
+func init() {
+ plugin.RegisterGlobalPlugin(NewFeikuaiPlugin())
+}
+
+// Search 同步搜索接口
+func (p *FeikuaiPlugin) Search(keyword string, ext map[string]interface{}) ([]model.SearchResult, error) {
+ result, err := p.SearchWithResult(keyword, ext)
+ if err != nil {
+ return nil, err
+ }
+ return result.Results, nil
+}
+
+// SearchWithResult 带结果统计的搜索接口
+func (p *FeikuaiPlugin) SearchWithResult(keyword string, ext map[string]interface{}) (model.PluginSearchResult, error) {
+ return p.AsyncSearchWithResult(keyword, p.searchImpl, p.MainCacheKey, ext)
+}
+
+// searchImpl 搜索实现
+func (p *FeikuaiPlugin) searchImpl(client *http.Client, keyword string, ext map[string]interface{}) ([]model.SearchResult, error) {
+ // 使用优化的客户端
+ if p.optimizedClient != nil {
+ client = p.optimizedClient
+ }
+
+ // 构建API搜索URL
+ searchURL := fmt.Sprintf(SearchAPIURL, url.QueryEscape(keyword))
+
+ // 创建带超时的上下文
+ ctx, cancel := context.WithTimeout(context.Background(), DefaultTimeout)
+ defer cancel()
+
+ // 创建请求
+ req, err := http.NewRequestWithContext(ctx, "GET", searchURL, nil)
+ if err != nil {
+ return nil, fmt.Errorf("[%s] 创建请求失败: %w", p.Name(), 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")
+ req.Header.Set("Accept", "application/json, text/plain, */*")
+ req.Header.Set("Accept-Language", "zh-CN,zh;q=0.9,en;q=0.8")
+ req.Header.Set("Connection", "keep-alive")
+ req.Header.Set("Referer", "https://feikuai.tv/")
+
+ // 发送请求(带重试)
+ resp, err := p.doRequestWithRetry(req, client)
+ if err != nil {
+ return nil, fmt.Errorf("[%s] 搜索请求失败: %w", p.Name(), err)
+ }
+ defer resp.Body.Close()
+
+ // 检查状态码
+ if resp.StatusCode != 200 {
+ return nil, fmt.Errorf("[%s] 请求返回状态码: %d", p.Name(), resp.StatusCode)
+ }
+
+ // 读取并解析JSON响应
+ body, err := io.ReadAll(resp.Body)
+ if err != nil {
+ return nil, fmt.Errorf("[%s] 读取响应失败: %w", p.Name(), err)
+ }
+
+ var apiResp FeikuaiAPIResponse
+ if err := json.Unmarshal(body, &apiResp); err != nil {
+ return nil, fmt.Errorf("[%s] JSON解析失败: %w", p.Name(), err)
+ }
+
+ // 检查API响应状态
+ if apiResp.Code != 0 {
+ return nil, fmt.Errorf("[%s] API返回错误: %s (code: %d)", p.Name(), apiResp.Msg, apiResp.Code)
+ }
+
+ // 解析搜索结果
+ var results []model.SearchResult
+ for _, item := range apiResp.Items {
+ // 每个item可能包含多个种子
+ for _, torrent := range item.Torrents {
+ result := p.parseTorrent(keyword, item, torrent)
+ if result.Title != "" && len(result.Links) > 0 {
+ results = append(results, result)
+ }
+ }
+ }
+
+ // 使用关键词过滤结果
+ return plugin.FilterResultsByKeyword(results, keyword), nil
+}
+
+// FeikuaiAPIResponse API响应结构
+type FeikuaiAPIResponse struct {
+ Code int `json:"code"`
+ Msg string `json:"msg"`
+ Keyword string `json:"keyword"`
+ Count int `json:"count"`
+ Items []FeikuaiAPIItem `json:"items"`
+}
+
+// FeikuaiAPIItem API数据项
+type FeikuaiAPIItem struct {
+ ContentID *string `json:"content_id"`
+ Title string `json:"title"`
+ Type string `json:"type"`
+ Year *int `json:"year"`
+ Torrents []FeikuaiTorrent `json:"torrents"`
+}
+
+// FeikuaiTorrent 种子数据
+type FeikuaiTorrent struct {
+ InfoHash string `json:"info_hash"`
+ Magnet string `json:"magnet"`
+ Name string `json:"name"`
+ SizeBytes int64 `json:"size_bytes"`
+ SizeGB float64 `json:"size_gb"`
+ Seeders int `json:"seeders"`
+ Leechers int `json:"leechers"`
+ PublishedAt string `json:"published_at"`
+ PublishedAgo string `json:"published_ago"`
+ FilePath string `json:"file_path"`
+ FileExt string `json:"file_ext"`
+}
+
+// parseTorrent 解析种子数据为SearchResult
+func (p *FeikuaiPlugin) parseTorrent(keyword string, item FeikuaiAPIItem, torrent FeikuaiTorrent) model.SearchResult {
+ // 构建唯一ID
+ uniqueID := fmt.Sprintf("%s-%s", p.Name(), torrent.InfoHash)
+
+ // 构建work_title
+ workTitle := p.buildWorkTitle(keyword, torrent.Name)
+
+ // 构建描述信息
+ content := p.buildContent(item, torrent)
+
+ // 解析发布时间
+ datetime := p.parsePublishedTime(torrent.PublishedAt)
+
+ // 构建标签
+ tags := p.extractTags(item.Title, torrent.Name)
+
+ // 构建磁力链接
+ links := []model.Link{
+ {
+ Type: "magnet",
+ URL: torrent.Magnet,
+ Password: "", // 磁力链接无密码
+ Datetime: datetime,
+ WorkTitle: workTitle,
+ },
+ }
+
+ return model.SearchResult{
+ UniqueID: uniqueID,
+ Title: workTitle, // 使用处理后的work_title作为标题
+ Content: content,
+ Links: links,
+ Tags: tags,
+ Channel: "", // 插件搜索结果Channel为空
+ Datetime: datetime,
+ }
+}
+
+// buildWorkTitle 构建work_title(核心功能)
+func (p *FeikuaiPlugin) buildWorkTitle(keyword, fileName string) string {
+ // 1. 清洗文件名
+ cleanedName := p.cleanFileName(fileName)
+
+ // 2. 检查是否包含关键词
+ if p.containsKeywords(keyword, cleanedName) {
+ return cleanedName
+ }
+
+ // 3. 不包含关键词,拼接中文关键词
+ return fmt.Sprintf("%s-%s", keyword, cleanedName)
+}
+
+// cleanFileName 清洗文件名
+func (p *FeikuaiPlugin) cleanFileName(fileName string) string {
+ // 去除文件扩展名
+ fileName = fileExtRegex.ReplaceAllString(fileName, "")
+
+ // 去除文件大小信息
+ fileName = fileSizeRegex.ReplaceAllString(fileName, "")
+
+ // 去除日期时间部分(@来源-日期 时间)
+ if idx := strings.Index(fileName, "@"); idx != -1 {
+ fileName = fileName[:idx]
+ }
+
+ return strings.TrimSpace(fileName)
+}
+
+// containsKeywords 检查文本是否包含关键词
+func (p *FeikuaiPlugin) containsKeywords(keyword, text string) bool {
+ // 简化处理:分词并检查
+ keywords := p.splitKeywords(keyword)
+ lowerText := strings.ToLower(text)
+
+ for _, kw := range keywords {
+ if strings.Contains(lowerText, strings.ToLower(kw)) {
+ return true
+ }
+ }
+
+ return false
+}
+
+// splitKeywords 分词提取关键词
+func (p *FeikuaiPlugin) splitKeywords(keyword string) []string {
+ // 移除标点符号和空格
+ keyword = strings.TrimSpace(keyword)
+
+ // 简单按空格、中文标点分割
+ separators := []string{" ", " ", ",", "。", "、", ";", ":", "!", "?", "-", "_"}
+
+ parts := []string{keyword}
+ for _, sep := range separators {
+ var newParts []string
+ for _, part := range parts {
+ if strings.Contains(part, sep) {
+ newParts = append(newParts, strings.Split(part, sep)...)
+ } else {
+ newParts = append(newParts, part)
+ }
+ }
+ parts = newParts
+ }
+
+ // 过滤空字符串和过短的词
+ var result []string
+ for _, part := range parts {
+ part = strings.TrimSpace(part)
+ if len(part) >= 2 { // 至少2个字符
+ result = append(result, part)
+ }
+ }
+
+ return result
+}
+
+// buildContent 构建内容描述
+func (p *FeikuaiPlugin) buildContent(item FeikuaiAPIItem, torrent FeikuaiTorrent) string {
+ var contentParts []string
+
+ // 文件名
+ contentParts = append(contentParts, fmt.Sprintf("文件名: %s", torrent.Name))
+
+ // 文件大小
+ contentParts = append(contentParts, fmt.Sprintf("大小: %.2f GB", torrent.SizeGB))
+
+ // 做种数和下载数
+ contentParts = append(contentParts, fmt.Sprintf("做种: %d", torrent.Seeders))
+ contentParts = append(contentParts, fmt.Sprintf("下载: %d", torrent.Leechers))
+
+ // 发布时间(人类可读格式)
+ if torrent.PublishedAgo != "" {
+ contentParts = append(contentParts, fmt.Sprintf("发布: %s", torrent.PublishedAgo))
+ }
+
+ return strings.Join(contentParts, " | ")
+}
+
+// extractTags 提取标签
+func (p *FeikuaiPlugin) extractTags(title, fileName string) []string {
+ var tags []string
+ combinedText := strings.ToUpper(title + " " + fileName)
+
+ // 分辨率标签
+ if strings.Contains(combinedText, "2160P") || strings.Contains(combinedText, "4K") {
+ tags = append(tags, "4K")
+ } else if strings.Contains(combinedText, "1080P") {
+ tags = append(tags, "1080P")
+ } else if strings.Contains(combinedText, "720P") {
+ tags = append(tags, "720P")
+ }
+
+ // 编码格式
+ if strings.Contains(combinedText, "H265") || strings.Contains(combinedText, "HEVC") {
+ tags = append(tags, "H265")
+ } else if strings.Contains(combinedText, "H264") || strings.Contains(combinedText, "AVC") {
+ tags = append(tags, "H264")
+ }
+
+ // HDR标签
+ if strings.Contains(combinedText, "HDR") {
+ tags = append(tags, "HDR")
+ }
+
+ // 60帧
+ if strings.Contains(combinedText, "60FPS") || strings.Contains(combinedText, "60HZ") {
+ tags = append(tags, "60fps")
+ }
+
+ return tags
+}
+
+// parsePublishedTime 解析发布时间
+func (p *FeikuaiPlugin) parsePublishedTime(timeStr string) time.Time {
+ if timeStr == "" {
+ return time.Now()
+ }
+
+ // 解析ISO 8601格式: "2025-11-18 00:54:20.659664+00"
+ layouts := []string{
+ "2006-01-02 15:04:05.999999-07",
+ "2006-01-02 15:04:05.999999+07",
+ "2006-01-02 15:04:05-07",
+ "2006-01-02 15:04:05+07",
+ "2006-01-02 15:04:05",
+ }
+
+ for _, layout := range layouts {
+ if t, err := time.Parse(layout, timeStr); err == nil {
+ return t
+ }
+ }
+
+ // 解析失败,返回当前时间
+ return time.Now()
+}
+
+// doRequestWithRetry 带重试的HTTP请求
+func (p *FeikuaiPlugin) doRequestWithRetry(req *http.Request, client *http.Client) (*http.Response, error) {
+ maxRetries := 3
+ var lastErr error
+
+ for i := 0; i < maxRetries; i++ {
+ if i > 0 {
+ // 指数退避
+ backoff := time.Duration(1<
+
+
+```
+
+### 单个搜索结果结构
+
+每个搜索结果包含以下主要元素:
+
+#### 1. 分类标签
+
+```html
+剧集
+```
+
+- 类型:电影、剧集、综艺、动漫
+
+#### 2. 封面图片和详情页链接
+
+```html
+
+
+
30集完结
+
豆瓣:9.3分
+
+

+
+
+
+```
+
+- **详情页链接**: 从 `` 标签的 `href` 属性提取
+- **资源ID**: 从URL中提取(如 `157546`)
+- **更新状态**: `.module-item-note` 包含集数信息
+- **豆瓣评分**: `.module-item-douban` 包含评分(可选)
+- **封面图片**: `img` 标签的 `data-original` 属性
+
+#### 3. 标题和基本信息
+
+```html
+
+```
+
+- **标题**: `.module-card-item-title strong` 的文本内容
+- **年份/地区/类型**: 第一个 `.module-info-item-content` 包含,用 `/` 分隔
+- **演员信息**: 第二个 `.module-info-item-content` 包含演员列表
+
+#### 4. 操作按钮
+
+```html
+
+```
+
+### 搜索结果数量
+
+```html
+
+ 搜索 "凡人修仙传",
+ 找到 26 部影片
+
+```
+
+- **搜索关键词**: `.module-heading-search-result strong` (第一个)
+- **结果数量**: `.mac_total` 的文本内容
+
+### 分页结构
+
+```html
+
+```
+
+## 详情页面结构
+
+### 1. 基本信息区域
+
+```html
+
+
+
+
+
+
![凡人修仙传]()
+
+
+
+
+
+
+```
+
+- **标题**: `h1` 标签的文本内容
+- **年份**: 第一个 `.module-info-tag-link a` 的 `title` 属性
+- **地区**: 第二个 `.module-info-tag-link a` 的 `title` 属性
+- **类型**: 第三个 `.module-info-tag-link` 内的所有 `a` 标签文本
+
+### 2. 详细信息
+
+```html
+
+```
+
+- **剧情简介**: `.module-info-introduction-content p` 的文本内容
+- **导演**: 查找包含 "导演:" 的 `.module-info-item-title`,然后提取 `.module-info-item-content` 中的演员链接
+- **主演**: 查找包含 "主演:" 的 `.module-info-item-title`,然后提取 `.module-info-item-content` 中的演员链接
+
+### 3. 网盘下载链接区域 ⭐ 核心
+
+```html
+
+
+
影片下载
+
+
+
+
+ 百度网盘
+ 1
+
+
+ 夸克网盘
+ 1
+
+
+
+
+
+
+
+```
+
+#### 网盘类型标签
+
+- **网盘类型**: `.module-tab-item span` 的文本内容
+- **数量**: `.module-tab-item small` 的文本内容
+- **网盘标识**: `data-dropdown-value` 属性或 `span` 文本
+
+支持的网盘/链接类型:
+- 百度网盘 (`baidu`)
+- 夸克网盘 (`quark`)
+- 迅雷云盘 (`xunlei`)
+- 阿里云盘 (`aliyun`)
+- 天翼云盘 (`tianyi`)
+- UC网盘 (`uc`)
+- 115网盘 (`115`)
+- 123云盘 (`123`)
+- 移动云盘 (`mobile`)
+- 磁力链接 (`magnet`)
+
+#### 下载链接列表
+
+```html
+
+```
+
+##### 链接数据提取
+
+- **下载链接URL**: `.module-row-text` 的 `href` 属性 或 `.module-row-title-dlist p` 的文本内容
+- **网盘/链接类型**: 根据链接URL自动识别
+ - 网盘链接:`baidu`, `quark`, `aliyun`, `xunlei`, `tianyi`, `uc`, `115`, `123`, `mobile`
+ - 磁力链接:`magnet:?xt=urn:btih:` 开头识别为 `magnet`
+
+- **独立标题** (⭐ 重要 - 对应API的 `work_title` 字段):
+ - **基础提取**: 从 `.module-row-title-dlist h4` 提取文本内容
+ - **清洗处理**:
+ 1. 去除末尾的日期时间部分(`@来源-日期 时间`)
+ 2. 去除文件扩展名(如 `.mkv`, `.mp4` 等)
+ 3. 去除文件大小信息(如 `· 2.29GB`)
+ - **标题拼接规则** (关键):
+ - 检查清洗后的独立标题是否包含详情页主标题的关键词
+ - **判断方法**: 将详情页标题分词,检查独立标题中是否包含任一关键词(忽略标点和空格)
+ - **需要拼接**: 如果不包含关键词,则拼接格式为 `{详情页主标题}-{独立标题}`
+ - **无需拼接**: 如果包含关键词,直接使用独立标题
+ - **示例**:
+ - 网盘链接:`凡人修仙传(2025)4K 高码率 更至EP169@一键搜片-2025-11-16 18:55:25`
+ → 清洗后:`凡人修仙传(2025)4K 高码率 更至EP169`
+ → 包含关键词"凡人修仙传",无需拼接
+ - 磁力链接:`Strange.Tales.of.Tang.Dynasty.S03E07.2025.2160p.IQ.WEB-DL.H265.DDP5.1-BlackTV.mkv · 2.29GB@一键搜片-2025-11-18 17:09:52`
+ → 详情页标题:`唐朝诡事录之长安`
+ → 清洗后:`Strange.Tales.of.Tang.Dynasty.S03E07.2025.2160p.IQ.WEB-DL.H265.DDP5.1-BlackTV`
+ → 不包含关键词,需要拼接
+ → 最终:`唐朝诡事录之长安-Strange.Tales.of.Tang.Dynasty.S03E07.2025.2160p.IQ.WEB-DL.H265.DDP5.1-BlackTV`
+
+- **日期提取** (对应API的 `datetime` 字段):
+ - 从独立标题中提取日期时间信息
+ - 日期格式:`@来源-YYYY-MM-DD HH:mm:ss`
+ - 正则表达式:`@[^-]+-(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})`
+ - 示例:从 `@一键搜片-2025-11-16 18:55:25` 提取 `2025-11-16 18:55:25`
+
+## 提取逻辑
+
+### 搜索结果页面提取逻辑
+
+1. 定位所有的 `.module-card-item.module-item` 元素
+2. 对于每个元素:
+ - 从 `.module-card-item-poster` 的 `href` 属性提取详情页链接
+ - 从链接中提取资源ID(如 `157546`)
+ - 从 `.module-card-item-title strong` 提取标题
+ - 从 `.module-card-item-class` 提取分类
+ - 从 `.module-item-note` 提取更新状态
+ - 从 `.module-item-douban` 提取豆瓣评分(可选)
+ - 从第一个 `.module-info-item-content` 提取年份/地区/类型
+ - 从第二个 `.module-info-item-content` 提取演员列表
+ - 从 `img` 的 `data-original` 属性提取封面图片URL
+
+### 详情页面提取逻辑
+
+1. 获取资源基本信息:
+ - 标题:`h1` 的文本内容
+ - 年份:第一个 `.module-info-tag-link a[title]` 的 `title` 属性
+ - 地区:第二个 `.module-info-tag-link a[title]` 的 `title` 属性
+ - 类型:第三个 `.module-info-tag-link` 内的所有 `a` 标签文本
+ - 封面图片:`.module-info-poster img` 的 `data-original` 属性
+
+2. 提取详细信息:
+ - 剧情简介:`.module-info-introduction-content p` 的文本内容
+ - 导演:查找包含 "导演:" 的 `.module-info-item`,提取其中的 `a` 标签文本
+ - 主演:查找包含 "主演:" 的 `.module-info-item`,提取其中的 `a` 标签文本
+
+3. 提取下载链接(⭐ 核心):
+ - 遍历所有 `.module-tab-item`,获取网盘类型和数量
+ - 对应每个 `.tab-content`,提取其中的 `.module-row-info`
+ - 对每个 `.module-row-info`:
+ - **链接URL**: 从 `.module-row-text` 的 `href` 属性或 `.module-row-title-dlist p` 提取
+ - **链接类型**: 根据链接URL自动识别(网盘类型或 `magnet`)
+ - **原始标题**: 从 `.module-row-title-dlist h4` 提取完整文本
+ - **独立标题** (`work_title`):
+ 1. 清洗原始标题(去除日期、扩展名、文件大小)
+ 2. 检查是否包含详情页主标题关键词
+ 3. 如不包含,拼接为 `{详情页主标题}-{清洗后标题}`
+ - **日期时间** (`datetime`): 从原始标题中提取日期,使用正则 `@[^-]+-(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})`
+ - **密码**: 从URL参数中提取(如 `?pwd=xxx` 或 `?password=xxx`,仅适用于部分网盘)
+
+## 网盘链接和磁力链接格式
+
+| 类型 | URL特征 | 密码格式 |
+|---------|---------|---------|
+| 百度网盘 | `pan.baidu.com` | `?pwd=` 参数 |
+| 夸克网盘 | `pan.quark.cn` | 无密码或单独提供 |
+| 阿里云盘 | `alipan.com` 或 `aliyundrive.com` | 无密码 |
+| 迅雷网盘 | `pan.xunlei.com` | `?pwd=` 参数 |
+| 天翼云盘 | `cloud.189.cn` | 无密码 |
+| UC网盘 | `drive.uc.cn` | 无密码 |
+| 115网盘 | `115cdn.com` | `?password=` 参数 |
+| 123网盘 | `123684.com`, `123685.com`, `123912.com` | 无密码 |
+| 移动云盘 | `caiyun.139.com` | 无密码 |
+| 磁力链接 | `magnet:?xt=urn:btih:` | 无密码 |
+
+## API字段映射
+
+根据README的API文档,Link对象字段映射关系:
+
+| API字段 | HTML提取位置 | 提取方法 | 示例 |
+|---------|------------|---------|------|
+| `type` | 链接URL | 自动识别URL特征 | `baidu`, `quark`, `tianyi`, `magnet` 等 |
+| `url` | `.module-row-title-dlist p` 或 `href` | 文本内容或属性值 | `https://pan.baidu.com/s/xxx` 或 `magnet:?xt=...` |
+| `password` | 链接URL参数 | 提取 `?pwd=` 或 `?password=` | `B5B3`, `yyds` (仅部分网盘) |
+| `datetime` | `.module-row-title-dlist h4` | 正则提取日期时间 | `2025-11-16 18:55:25` |
+| `work_title` | `.module-row-title-dlist h4` + 详情页主标题 | 清洗+关键词检查+拼接 | 见下方详细说明 |
+
+**`work_title` 字段详细处理流程**:
+
+1. **提取原始标题**: 从 `.module-row-title-dlist h4` 获取完整文本
+ - 示例1: `凡人修仙传(2025)4K 高码率 更至EP169@一键搜片-2025-11-16 18:55:25`
+ - 示例2: `Strange.Tales.of.Tang.Dynasty.S03E07.2025.2160p.IQ.WEB-DL.H265.DDP5.1-BlackTV.mkv · 2.29GB@一键搜片-2025-11-18 17:09:52`
+
+2. **清洗标题**:
+ - 去除日期时间部分: 删除 `@来源-日期 时间` 格式的后缀
+ - 去除文件扩展名: 删除 `.mkv`, `.mp4`, `.avi` 等
+ - 去除文件大小: 删除 `· 2.29GB` 等文件大小信息
+ - 清洗结果1: `凡人修仙传(2025)4K 高码率 更至EP169`
+ - 清洗结果2: `Strange.Tales.of.Tang.Dynasty.S03E07.2025.2160p.IQ.WEB-DL.H265.DDP5.1-BlackTV`
+
+3. **关键词检查与拼接**:
+ - 获取详情页主标题(如 `唐朝诡事录之长安`)
+ - 将主标题分词,提取关键词(忽略标点符号和空格)
+ - 检查清洗后的独立标题是否包含任一关键词
+ - **包含关键词**: 直接使用清洗后的标题
+ - 示例: `凡人修仙传(2025)4K 高码率 更至EP169` (包含"凡人修仙传")
+ - **不包含关键词**: 拼接格式为 `{详情页主标题}-{清洗后标题}`
+ - 示例: `唐朝诡事录之长安-Strange.Tales.of.Tang.Dynasty.S03E07.2025.2160p.IQ.WEB-DL.H265.DDP5.1-BlackTV`
+
+**其他字段说明**:
+- `datetime`: 从原始 `h4` 标题中提取的时间戳,格式为 `YYYY-MM-DD HH:mm:ss`
+- `password`: 部分网盘(百度、迅雷、115)的密码在URL参数中,需要单独提取;磁力链接无密码
+
+## 注意事项
+
+1. **图片延迟加载**: 封面图片使用了 `lazy lazyload` 类,实际图片URL在 `data-original` 属性中
+
+2. **资源ID提取**: 从URL中提取ID的正则表达式:`/voddetail/(\d+)\.html`
+
+3. **链接类型识别**:
+ - 网盘链接:通过域名识别(`pan.baidu.com`, `pan.quark.cn` 等)
+ - 磁力链接:通过 `magnet:?xt=urn:btih:` 前缀识别
+
+4. **网盘链接密码**: 某些网盘的密码包含在URL参数中(如 `?pwd=B5B3`),需要分离链接和密码;磁力链接无密码
+
+5. **独立标题处理** (⭐ 核心重点):
+ - 每个链接都有独立的 `h4` 标题,必须单独提取
+ - 需要清洗标题(去除日期、扩展名、文件大小)
+ - **关键词检查**: 必须检查清洗后标题是否包含详情页主标题的关键词
+ - **拼接规则**: 不包含关键词时,需拼接为 `{详情页主标题}-{清洗后标题}`
+ - 特别注意磁力链接的标题通常是英文文件名,大概率需要拼接中文标题
+
+6. **日期时间提取** (重要):
+ - 从 `h4` 标题末尾提取日期时间
+ - 格式为 `@来源-YYYY-MM-DD HH:mm:ss`
+ - 正则表达式: `@[^-]+-(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})`
+
+7. **多链接支持**: 一个资源可能有多个网盘和磁力链接,每个链接都有独立的标题、时间和密码
+
+8. **分页处理**: 搜索结果有分页,URL格式为 `/vodsearch/{关键词}----------{页码}---.html`
+
+9. **AJAX加载**: 网站使用AJAX动态加载搜索结果,需要注意异步请求处理
+
+10. **反爬虫**: 图片设置了 `referrerpolicy="no-referrer"`,需要在请求头中处理
diff --git a/plugin/feikuai/json结构分析.md b/plugin/feikuai/json结构分析.md
new file mode 100644
index 0000000..99ce9a2
--- /dev/null
+++ b/plugin/feikuai/json结构分析.md
@@ -0,0 +1,299 @@
+# Feikuai API 数据结构分析
+
+## 基本信息
+- **数据源类型**: JSON API
+- **网站名称**: 飞快TV (feikuai.tv)
+- **API URL格式**: `https://feikuai.tv/t_search/bm_search.php?kw={URL编码的关键词}`
+- **数据特点**: 磁力链接搜索API,提供结构化的BT/磁力资源数据
+- **特殊说明**: 专注于磁力链接,包含详细的种子信息(做种数、下载数等)
+
+## API响应结构
+
+### 顶层结构
+```json
+{
+ "code": 0, // 状态码:0表示成功
+ "msg": "ok", // 响应消息
+ "keyword": "唐朝诡事录之长安", // 搜索关键词
+ "count": 8, // 搜索结果总数
+ "items": [] // 数据列表数组
+}
+```
+
+### `items`数组中的数据项结构
+```json
+{
+ "content_id": null, // 内容ID(通常为null)
+ "title": "【高清剧集网发布 www.BPHDTV.com】唐朝诡事录之长安[第07-08集][国语音轨+简繁英字幕].2025.2160p.IQ.WEB-DL.H265.DDP5.1-BlackTV",
+ "type": "movie", // 资源类型(通常为movie)
+ "year": null, // 年份(通常为null)
+ "torrents": [] // 磁力链接数组
+}
+```
+
+### `torrents`数组中的种子数据结构
+```json
+{
+ "info_hash": "c3a3a53c2408396d64450046361f00650cb9e53e", // 种子哈希值
+ "magnet": "magnet:?xt=urn:btih:C3A3A53C2408396D64450046361F00650CB9E53E&dn=Strange.Tales.of.Tang.Dynasty.S03E07.2025.2160p.IQ.WEB-DL.H265.DDP5.1-BlackTV.mkv&xl=2458041664",
+ "name": "Strange.Tales.of.Tang.Dynasty.S03E07.2025.2160p.IQ.WEB-DL.H265.DDP5.1-BlackTV.mkv",
+ "size_bytes": 2458041664, // 文件大小(字节)
+ "size_gb": 2.29, // 文件大小(GB)
+ "seeders": 4, // 做种数
+ "leechers": 4, // 下载数
+ "published_at": "2025-11-18 00:54:20.659664+00", // 发布时间(带时区)
+ "published_ago": "约 8 小时前", // 发布时间(人类可读)
+ "file_path": "Strange.Tales.of.Tang.Dynasty.S03E07.2025.2160p.IQ.WEB-DL.H265.DDP5.1-BlackTV.mkv",
+ "file_ext": "mkv" // 文件扩展名
+}
+```
+
+## 插件所需字段映射
+
+| 源字段 | 目标字段 | 说明 |
+|--------|----------|------|
+| `content_id` 或基于 `info_hash` | `UniqueID` | 格式: `feikuai-{info_hash}` 或 `feikuai-{index}` |
+| `title` | `Title` | 资源标题(包含发布组信息) |
+| `title` + `name` + `size_gb` + `seeders` + `leechers` | `Content` | 组合描述信息 |
+| 从 `title` 或 `name` 提取 | `Tags` | 标签数组(如分辨率、格式等) |
+| `torrents` | `Links` | 解析为Link数组,每个种子对应一个Link |
+| `""` | `Channel` | 插件搜索结果Channel为空 |
+| `published_at` | `Datetime` | 磁力链接发布时间 |
+
+## 下载链接解析
+
+### 磁力链接特点
+- **链接类型**: 全部为 `magnet` 类型
+- **无需密码**: 磁力链接不需要提取码
+- **多种子支持**: 一个资源(item)可能包含多个种子(torrents)
+
+### 磁力链接格式
+```
+magnet:?xt=urn:btih:{INFO_HASH}&dn={URL编码的文件名}&xl={文件大小}
+```
+
+**示例**:
+```
+magnet:?xt=urn:btih:C3A3A53C2408396D64450046361F00650CB9E53E&dn=Strange.Tales.of.Tang.Dynasty.S03E07.2025.2160p.IQ.WEB-DL.H265.DDP5.1-BlackTV.mkv&xl=2458041664
+```
+
+### 种子信息提取
+从 `torrents` 数组中,每个种子可提取以下信息:
+- **磁力链接**: `magnet` 字段
+- **文件名**: `name` 或 `file_path` 字段
+- **文件大小**: `size_gb` (GB) 或 `size_bytes` (字节)
+- **做种/下载数**: `seeders` / `leechers`
+- **发布时间**: `published_at` 或 `published_ago`
+
+## work_title 处理规则
+
+根据HTML结构分析中的规则,需要对每个磁力链接的标题进行处理:
+
+### 处理流程
+1. **提取标题**: 从 `name` 或 `file_path` 字段获取文件名
+2. **清洗标题**:
+ - 去除文件扩展名(`.mkv`, `.mp4` 等)
+ - 去除文件大小信息(如果在文件名中)
+3. **关键词检查**:
+ - 检查清洗后的文件名是否包含搜索关键词
+ - 或检查是否包含 `title` 字段中的关键词
+4. **拼接规则**:
+ - **包含关键词**: 直接使用清洗后的文件名
+ - **不包含关键词**: 拼接为 `{搜索关键词}-{清洗后文件名}`
+
+### 示例
+
+**场景1: 英文文件名,不包含中文关键词**
+```
+搜索关键词: "唐朝诡事录之长安"
+文件名: "Strange.Tales.of.Tang.Dynasty.S03E07.2025.2160p.IQ.WEB-DL.H265.DDP5.1-BlackTV.mkv"
+清洗后: "Strange.Tales.of.Tang.Dynasty.S03E07.2025.2160p.IQ.WEB-DL.H265.DDP5.1-BlackTV"
+work_title: "唐朝诡事录之长安-Strange.Tales.of.Tang.Dynasty.S03E07.2025.2160p.IQ.WEB-DL.H265.DDP5.1-BlackTV"
+```
+
+**场景2: 中文文件名,包含关键词**
+```
+搜索关键词: "唐朝诡事录之长安"
+文件名: "唐朝诡事录之长安.Horror.Stories.of.Tang.Dynasty.S03E05.2022.2160p.WEB-DL.H265.DDP5.1-ColorTV.mkv"
+清洗后: "唐朝诡事录之长安.Horror.Stories.of.Tang.Dynasty.S03E05.2022.2160p.WEB-DL.H265.DDP5.1-ColorTV"
+work_title: "唐朝诡事录之长安.Horror.Stories.of.Tang.Dynasty.S03E05.2022.2160p.WEB-DL.H265.DDP5.1-ColorTV"
+(包含关键词,无需拼接)
+```
+
+## 插件开发指导
+
+### 请求示例
+```go
+searchURL := fmt.Sprintf("https://feikuai.tv/t_search/bm_search.php?kw=%s", url.QueryEscape(keyword))
+```
+
+### SearchResult构建示例
+```go
+// 遍历items
+for _, item := range apiResponse.Items {
+ // 遍历每个item的torrents
+ for _, torrent := range item.Torrents {
+ // 清洗文件名
+ cleanedName := cleanFileName(torrent.Name)
+
+ // 检查是否包含关键词并拼接
+ workTitle := buildWorkTitle(keyword, cleanedName)
+
+ // 构建SearchResult
+ result := model.SearchResult{
+ UniqueID: fmt.Sprintf("feikuai-%s", torrent.InfoHash),
+ Title: item.Title, // 或使用workTitle
+ Content: buildContent(item, torrent),
+ Links: []model.Link{
+ {
+ Type: "magnet",
+ URL: torrent.Magnet,
+ Password: "", // 磁力链接无密码
+ Datetime: parseTime(torrent.PublishedAt),
+ WorkTitle: workTitle, // ⭐ 重要:独立标题
+ },
+ },
+ Tags: extractTags(item.Title, torrent.Name),
+ Channel: "", // 插件搜索结果Channel为空
+ Datetime: parseTime(torrent.PublishedAt),
+ }
+ results = append(results, result)
+ }
+}
+```
+
+### 关键函数示例
+
+#### 1. 清洗文件名
+```go
+func cleanFileName(fileName string) string {
+ // 去除文件扩展名
+ ext := filepath.Ext(fileName)
+ if ext != "" {
+ fileName = strings.TrimSuffix(fileName, ext)
+ }
+
+ // 去除文件大小信息(如果存在)
+ fileName = regexp.MustCompile(`\s*·\s*[\d.]+\s*[KMGT]B\s*$`).ReplaceAllString(fileName, "")
+
+ return strings.TrimSpace(fileName)
+}
+```
+
+#### 2. 构建work_title
+```go
+func buildWorkTitle(keyword, cleanedName string) string {
+ // 检查是否包含关键词(忽略大小写和标点)
+ if containsKeywords(keyword, cleanedName) {
+ return cleanedName
+ }
+
+ // 不包含关键词,需要拼接
+ return fmt.Sprintf("%s-%s", keyword, cleanedName)
+}
+
+func containsKeywords(keyword, text string) bool {
+ // 简单实现:分词后检查
+ keywords := splitKeywords(keyword)
+ for _, kw := range keywords {
+ if strings.Contains(strings.ToLower(text), strings.ToLower(kw)) {
+ return true
+ }
+ }
+ return false
+}
+```
+
+#### 3. 构建内容描述
+```go
+func buildContent(item FeikuaiAPIItem, torrent Torrent) string {
+ var contentParts []string
+
+ contentParts = append(contentParts, fmt.Sprintf("文件名: %s", torrent.Name))
+ contentParts = append(contentParts, fmt.Sprintf("大小: %.2f GB", torrent.SizeGB))
+ contentParts = append(contentParts, fmt.Sprintf("做种: %d", torrent.Seeders))
+ contentParts = append(contentParts, fmt.Sprintf("下载: %d", torrent.Leechers))
+ contentParts = append(contentParts, fmt.Sprintf("发布: %s", torrent.PublishedAgo))
+
+ return strings.Join(contentParts, " | ")
+}
+```
+
+#### 4. 提取标签
+```go
+func extractTags(title, fileName string) []string {
+ var tags []string
+
+ // 提取分辨率
+ if strings.Contains(title, "2160p") || strings.Contains(fileName, "2160p") {
+ tags = append(tags, "4K")
+ } else if strings.Contains(title, "1080p") || strings.Contains(fileName, "1080p") {
+ tags = append(tags, "1080p")
+ }
+
+ // 提取编码格式
+ if strings.Contains(title, "H265") || strings.Contains(fileName, "H265") {
+ tags = append(tags, "H265")
+ }
+
+ // 提取HDR
+ if strings.Contains(title, "HDR") || strings.Contains(fileName, "HDR") {
+ tags = append(tags, "HDR")
+ }
+
+ return tags
+}
+```
+
+#### 5. 时间解析
+```go
+func parseTime(timeStr string) time.Time {
+ // 解析ISO 8601格式: "2025-11-18 00:54:20.659664+00"
+ t, err := time.Parse("2006-01-02 15:04:05.999999-07", timeStr)
+ if err != nil {
+ // 解析失败,返回当前时间
+ return time.Now()
+ }
+ return t
+}
+```
+
+## API字段映射表
+
+| API字段 | Link对象字段 | 提取方法 | 示例 |
+|---------|-------------|---------|------|
+| `magnet` | `URL` | 直接使用 | `magnet:?xt=urn:btih:...` |
+| - | `Type` | 固定值 | `magnet` |
+| - | `Password` | 固定值 | `""` (空字符串) |
+| `published_at` | `Datetime` | 时间解析 | `2025-11-18T00:54:20Z` |
+| `name` | `WorkTitle` | 清洗+关键词检查+拼接 | `唐朝诡事录之长安-Strange.Tales...` |
+
+## 与其他插件的差异
+
+| 特性 | feikuai | wanou/ouge/zhizhen | huban | 说明 |
+|------|---------|-------------------|-------|------|
+| **链接类型** | 仅磁力链接 | 网盘链接 | 网盘链接 | 专注BT资源 |
+| **多链接** | 一对多 | 多对一 | 多对多 | 一个资源多个种子 |
+| **种子信息** | 详细 | 无 | 无 | 包含做种数等 |
+| **work_title** | 必需拼接 | 可选 | 可选 | 文件名通常不含中文 |
+| **时间信息** | 精确 | 当前时间 | 当前时间 | API提供发布时间 |
+
+## 注意事项
+
+1. **磁力链接专用**: 此API仅返回磁力链接,不包含网盘链接
+2. **多种子处理**: 一个资源可能有多个种子,需要全部提取
+3. **文件名处理**: 文件名通常是英文,需要拼接中文关键词
+4. **时区处理**: `published_at` 包含时区信息(+00),需要正确解析
+5. **做种数排序**: 建议按做种数(seeders)降序排序,优先显示热门资源
+6. **空值处理**: `content_id` 和 `year` 通常为 null,需要处理
+7. **标题清洗**: `title` 字段包含发布组信息(如【高清剧集网发布 www.BPHDTV.com】),可选择性去除
+
+## 开发建议
+
+1. **独立实现**: 不能复用网盘类插件的代码,需要专门处理磁力链接
+2. **work_title关键**: 文件名拼接中文关键词是核心功能
+3. **种子排序**: 实现按做种数排序,提升用户体验
+4. **时间解析**: 正确解析带时区的ISO 8601时间格式
+5. **内容丰富**: 充分利用API提供的文件大小、做种数等信息
+6. **错误处理**: API可能返回 `code != 0` 的错误状态
+7. **测试覆盖**: 重点测试中英文文件名的work_title拼接逻辑