new_search (#238)
Co-authored-by: Buf Generate <buf-generate@bondee.com>
This commit is contained in:
@@ -3,6 +3,8 @@ package main
|
|||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/xpzouying/xiaohongshu-mcp/xiaohongshu"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
@@ -117,7 +119,24 @@ func (s *AppServer) listFeedsHandler(c *gin.Context) {
|
|||||||
|
|
||||||
// searchFeedsHandler 搜索Feeds
|
// searchFeedsHandler 搜索Feeds
|
||||||
func (s *AppServer) searchFeedsHandler(c *gin.Context) {
|
func (s *AppServer) searchFeedsHandler(c *gin.Context) {
|
||||||
keyword := c.Query("keyword")
|
var keyword string
|
||||||
|
var filters []xiaohongshu.FilterOption
|
||||||
|
|
||||||
|
switch c.Request.Method {
|
||||||
|
case http.MethodPost:
|
||||||
|
// 对于POST请求,从JSON中获取keyword
|
||||||
|
var searchReq SearchFeedsRequest
|
||||||
|
if err := c.ShouldBindJSON(&searchReq); err != nil {
|
||||||
|
respondError(c, http.StatusBadRequest, "INVALID_REQUEST",
|
||||||
|
"请求参数错误", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
keyword = searchReq.Keyword
|
||||||
|
filters = searchReq.Filters
|
||||||
|
default:
|
||||||
|
keyword = c.Query("keyword")
|
||||||
|
}
|
||||||
|
|
||||||
if keyword == "" {
|
if keyword == "" {
|
||||||
respondError(c, http.StatusBadRequest, "MISSING_KEYWORD",
|
respondError(c, http.StatusBadRequest, "MISSING_KEYWORD",
|
||||||
"缺少关键词参数", "keyword parameter is required")
|
"缺少关键词参数", "keyword parameter is required")
|
||||||
@@ -125,7 +144,7 @@ func (s *AppServer) searchFeedsHandler(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 搜索 Feeds
|
// 搜索 Feeds
|
||||||
result, err := s.xiaohongshuService.SearchFeeds(c.Request.Context(), keyword)
|
result, err := s.xiaohongshuService.SearchFeeds(c.Request.Context(), keyword, filters...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
respondError(c, http.StatusInternalServerError, "SEARCH_FEEDS_FAILED",
|
respondError(c, http.StatusInternalServerError, "SEARCH_FEEDS_FAILED",
|
||||||
"搜索Feeds失败", err.Error())
|
"搜索Feeds失败", err.Error())
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
|
"github.com/xpzouying/xiaohongshu-mcp/xiaohongshu"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
@@ -223,12 +224,10 @@ func (s *AppServer) handleListFeeds(ctx context.Context) *MCPToolResult {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// handleSearchFeeds 处理搜索Feeds
|
// handleSearchFeeds 处理搜索Feeds
|
||||||
func (s *AppServer) handleSearchFeeds(ctx context.Context, args map[string]interface{}) *MCPToolResult {
|
func (s *AppServer) handleSearchFeeds(ctx context.Context, args SearchFeedsArgs) *MCPToolResult {
|
||||||
logrus.Info("MCP: 搜索Feeds")
|
logrus.Info("MCP: 搜索Feeds")
|
||||||
|
|
||||||
// 解析参数
|
if args.Keyword == "" {
|
||||||
keyword, ok := args["keyword"].(string)
|
|
||||||
if !ok || keyword == "" {
|
|
||||||
return &MCPToolResult{
|
return &MCPToolResult{
|
||||||
Content: []MCPContent{{
|
Content: []MCPContent{{
|
||||||
Type: "text",
|
Type: "text",
|
||||||
@@ -238,9 +237,23 @@ func (s *AppServer) handleSearchFeeds(ctx context.Context, args map[string]inter
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
logrus.Infof("MCP: 搜索Feeds - 关键词: %s", keyword)
|
logrus.Infof("MCP: 搜索Feeds - 关键词: %s, 筛选条件数量: %d", args.Keyword, len(args.Filters))
|
||||||
|
var filters []xiaohongshu.FilterOption
|
||||||
result, err := s.xiaohongshuService.SearchFeeds(ctx, keyword)
|
for _, filter := range args.Filters {
|
||||||
|
filterOption, err := xiaohongshu.NewFilterOption(xiaohongshu.GetFilterGroupIndex(filter.FiltersIndex), filter.TagsIndex)
|
||||||
|
if err != nil {
|
||||||
|
return &MCPToolResult{
|
||||||
|
Content: []MCPContent{{
|
||||||
|
Type: "text",
|
||||||
|
Text: fmt.Sprintf("搜索Feeds失败: 筛选组 %v 的标签索引 %v 错误: %v",
|
||||||
|
filter.FiltersIndex, filter.TagsIndex, err),
|
||||||
|
}},
|
||||||
|
IsError: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
filters = append(filters, filterOption)
|
||||||
|
}
|
||||||
|
result, err := s.xiaohongshuService.SearchFeeds(ctx, args.Keyword, filters...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &MCPToolResult{
|
return &MCPToolResult{
|
||||||
Content: []MCPContent{{
|
Content: []MCPContent{{
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package main
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
|
|
||||||
"github.com/modelcontextprotocol/go-sdk/mcp"
|
"github.com/modelcontextprotocol/go-sdk/mcp"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
@@ -28,7 +27,13 @@ type PublishVideoArgs struct {
|
|||||||
|
|
||||||
// SearchFeedsArgs 搜索内容的参数
|
// SearchFeedsArgs 搜索内容的参数
|
||||||
type SearchFeedsArgs struct {
|
type SearchFeedsArgs struct {
|
||||||
Keyword string `json:"keyword" jsonschema:"搜索关键词"`
|
Keyword string `json:"keyword" jsonschema:"搜索关键词"`
|
||||||
|
Filters []FilterOption `json:"filters,omitempty" jsonschema:"筛选选项列表"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type FilterOption struct {
|
||||||
|
FiltersIndex string `json:"filters_index" jsonschema:"筛选索引 排序依据 笔记类型, 发布时间, 搜索范围, 位置距离"` //
|
||||||
|
TagsIndex string `json:"tags_index" jsonschema:"筛选值 排序依据(综合、最新、最多点赞、最多评论、最多收藏)笔记类型(不限、视频、图文)发布时间(不限、一天内、一周内、半年内)搜索范围(不限、已看过、未看过、已关注)位置距离(不限、同城、附近)"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// FeedDetailArgs 获取Feed详情的参数
|
// FeedDetailArgs 获取Feed详情的参数
|
||||||
@@ -147,10 +152,7 @@ func registerTools(server *mcp.Server, appServer *AppServer) {
|
|||||||
Description: "搜索小红书内容(需要已登录)",
|
Description: "搜索小红书内容(需要已登录)",
|
||||||
},
|
},
|
||||||
func(ctx context.Context, req *mcp.CallToolRequest, args SearchFeedsArgs) (*mcp.CallToolResult, any, error) {
|
func(ctx context.Context, req *mcp.CallToolRequest, args SearchFeedsArgs) (*mcp.CallToolResult, any, error) {
|
||||||
argsMap := map[string]interface{}{
|
result := appServer.handleSearchFeeds(ctx, args)
|
||||||
"keyword": args.Keyword,
|
|
||||||
}
|
|
||||||
result := appServer.handleSearchFeeds(ctx, argsMap)
|
|
||||||
return convertToMCPResult(result), nil, nil
|
return convertToMCPResult(result), nil, nil
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ func setupRoutes(appServer *AppServer) *gin.Engine {
|
|||||||
api.POST("/publish_video", appServer.publishVideoHandler)
|
api.POST("/publish_video", appServer.publishVideoHandler)
|
||||||
api.GET("/feeds/list", appServer.listFeedsHandler)
|
api.GET("/feeds/list", appServer.listFeedsHandler)
|
||||||
api.GET("/feeds/search", appServer.searchFeedsHandler)
|
api.GET("/feeds/search", appServer.searchFeedsHandler)
|
||||||
|
api.POST("/feeds/search", appServer.searchFeedsHandler)
|
||||||
api.POST("/feeds/detail", appServer.getFeedDetailHandler)
|
api.POST("/feeds/detail", appServer.getFeedDetailHandler)
|
||||||
api.POST("/user/profile", appServer.userProfileHandler)
|
api.POST("/user/profile", appServer.userProfileHandler)
|
||||||
api.POST("/feeds/comment", appServer.postCommentHandler)
|
api.POST("/feeds/comment", appServer.postCommentHandler)
|
||||||
|
|||||||
@@ -295,7 +295,7 @@ func (s *XiaohongshuService) ListFeeds(ctx context.Context) (*FeedsListResponse,
|
|||||||
return response, nil
|
return response, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *XiaohongshuService) SearchFeeds(ctx context.Context, keyword string) (*FeedsListResponse, error) {
|
func (s *XiaohongshuService) SearchFeeds(ctx context.Context, keyword string, filters ...xiaohongshu.FilterOption) (*FeedsListResponse, error) {
|
||||||
b := newBrowser()
|
b := newBrowser()
|
||||||
defer b.Close()
|
defer b.Close()
|
||||||
|
|
||||||
@@ -304,7 +304,7 @@ func (s *XiaohongshuService) SearchFeeds(ctx context.Context, keyword string) (*
|
|||||||
|
|
||||||
action := xiaohongshu.NewSearchAction(page)
|
action := xiaohongshu.NewSearchAction(page)
|
||||||
|
|
||||||
feeds, err := action.Search(ctx, keyword)
|
feeds, err := action.Search(ctx, keyword, filters...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
7
types.go
7
types.go
@@ -1,5 +1,7 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
|
import "github.com/xpzouying/xiaohongshu-mcp/xiaohongshu"
|
||||||
|
|
||||||
// HTTP API 响应类型
|
// HTTP API 响应类型
|
||||||
|
|
||||||
// ErrorResponse 错误响应
|
// ErrorResponse 错误响应
|
||||||
@@ -38,6 +40,11 @@ type FeedDetailRequest struct {
|
|||||||
XsecToken string `json:"xsec_token" binding:"required"`
|
XsecToken string `json:"xsec_token" binding:"required"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type SearchFeedsRequest struct {
|
||||||
|
Keyword string `json:"keyword" binding:"required"`
|
||||||
|
Filters []xiaohongshu.FilterOption `json:"filters" binding:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
// FeedDetailResponse Feed详情响应
|
// FeedDetailResponse Feed详情响应
|
||||||
type FeedDetailResponse struct {
|
type FeedDetailResponse struct {
|
||||||
FeedID string `json:"feed_id"`
|
FeedID string `json:"feed_id"`
|
||||||
|
|||||||
@@ -16,6 +16,131 @@ type SearchResult struct {
|
|||||||
} `json:"search"`
|
} `json:"search"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type FilterOption struct {
|
||||||
|
FiltersIndex int `json:"filters_index" jsonschema:"筛选组索引 1=排序依据, 2=笔记类型, 3=发布时间, 4=搜索范围, 5=位置距离"`
|
||||||
|
TagsIndex int `json:"tags_index" jsonschema:"标签索引,根据不同的筛选组索引对应不同的选项: 1=排序依据(1-5), 2=笔记类型(1-3), 3=发布时间(1-4), 4=搜索范围(1-4), 5=位置距离(1-3)"`
|
||||||
|
Text string `json:"text" jsonschema:"标签文本描述"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 预定义的筛选选项映射表
|
||||||
|
var FilterOptionsMap = map[int][]FilterOption{
|
||||||
|
1: { // 排序依据
|
||||||
|
{FiltersIndex: 1, TagsIndex: 1, Text: "综合"},
|
||||||
|
{FiltersIndex: 1, TagsIndex: 2, Text: "最新"},
|
||||||
|
{FiltersIndex: 1, TagsIndex: 3, Text: "最多点赞"},
|
||||||
|
{FiltersIndex: 1, TagsIndex: 4, Text: "最多评论"},
|
||||||
|
{FiltersIndex: 1, TagsIndex: 5, Text: "最多收藏"},
|
||||||
|
},
|
||||||
|
2: { // 笔记类型
|
||||||
|
{FiltersIndex: 2, TagsIndex: 1, Text: "不限"},
|
||||||
|
{FiltersIndex: 2, TagsIndex: 2, Text: "视频"},
|
||||||
|
{FiltersIndex: 2, TagsIndex: 3, Text: "图文"},
|
||||||
|
},
|
||||||
|
3: { // 发布时间
|
||||||
|
{FiltersIndex: 3, TagsIndex: 1, Text: "不限"},
|
||||||
|
{FiltersIndex: 3, TagsIndex: 2, Text: "一天内"},
|
||||||
|
{FiltersIndex: 3, TagsIndex: 3, Text: "一周内"},
|
||||||
|
{FiltersIndex: 3, TagsIndex: 4, Text: "半年内"},
|
||||||
|
},
|
||||||
|
4: { // 搜索范围
|
||||||
|
{FiltersIndex: 4, TagsIndex: 1, Text: "不限"},
|
||||||
|
{FiltersIndex: 4, TagsIndex: 2, Text: "已看过"},
|
||||||
|
{FiltersIndex: 4, TagsIndex: 3, Text: "未看过"},
|
||||||
|
{FiltersIndex: 4, TagsIndex: 4, Text: "已关注"},
|
||||||
|
},
|
||||||
|
5: { // 位置距离
|
||||||
|
{FiltersIndex: 5, TagsIndex: 1, Text: "不限"},
|
||||||
|
{FiltersIndex: 5, TagsIndex: 2, Text: "同城"},
|
||||||
|
{FiltersIndex: 5, TagsIndex: 3, Text: "附近"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// 定义筛选组索引到中文描述的映射
|
||||||
|
var filterGroupMap = map[int]string{
|
||||||
|
1: "排序依据",
|
||||||
|
2: "笔记类型",
|
||||||
|
3: "发布时间",
|
||||||
|
4: "搜索范围",
|
||||||
|
5: "位置距离",
|
||||||
|
}
|
||||||
|
|
||||||
|
// validateFilterOption 验证筛选选项是否在有效范围内
|
||||||
|
func validateFilterOption(filter FilterOption) error {
|
||||||
|
// 检查筛选组索引是否有效
|
||||||
|
if filter.FiltersIndex < 1 || filter.FiltersIndex > 5 {
|
||||||
|
return fmt.Errorf("无效的筛选组索引 %d,有效范围为 1-5", filter.FiltersIndex)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查标签索引是否在对应筛选组的有效范围内
|
||||||
|
options, exists := FilterOptionsMap[filter.FiltersIndex]
|
||||||
|
if !exists {
|
||||||
|
return fmt.Errorf("筛选组 %d 不存在", filter.FiltersIndex)
|
||||||
|
}
|
||||||
|
|
||||||
|
if filter.TagsIndex < 1 || filter.TagsIndex > len(options) {
|
||||||
|
return fmt.Errorf("筛选组 %d 的标签索引 %d 超出范围,有效范围为 1-%d",
|
||||||
|
filter.FiltersIndex, filter.TagsIndex, len(options))
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 便利函数:根据文本创建筛选选项
|
||||||
|
func NewFilterOption(filtersIndex int, text string) (FilterOption, error) {
|
||||||
|
options, exists := FilterOptionsMap[filtersIndex]
|
||||||
|
if !exists {
|
||||||
|
return FilterOption{}, fmt.Errorf("筛选组 %d 不存在", filtersIndex)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, option := range options {
|
||||||
|
if option.Text == text {
|
||||||
|
return option, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return FilterOption{}, fmt.Errorf("在筛选组 %d 中未找到文本 '%s'", filtersIndex, text)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 便利函数:创建常用的筛选选项
|
||||||
|
func SortBy(text string) (FilterOption, error) {
|
||||||
|
return NewFilterOption(1, text) // 排序依据
|
||||||
|
}
|
||||||
|
|
||||||
|
func NoteType(text string) (FilterOption, error) {
|
||||||
|
return NewFilterOption(2, text) // 笔记类型
|
||||||
|
}
|
||||||
|
|
||||||
|
func TimeRange(text string) (FilterOption, error) {
|
||||||
|
return NewFilterOption(3, text) // 发布时间
|
||||||
|
}
|
||||||
|
|
||||||
|
func SearchScope(text string) (FilterOption, error) {
|
||||||
|
return NewFilterOption(4, text) // 搜索范围
|
||||||
|
}
|
||||||
|
|
||||||
|
func LocationDistance(text string) (FilterOption, error) {
|
||||||
|
return NewFilterOption(5, text) // 位置距离
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetFilterGroupDescription 根据筛选组索引获取中文描述
|
||||||
|
func GetFilterGroupDescription(index int) string {
|
||||||
|
if desc, exists := filterGroupMap[index]; exists {
|
||||||
|
return desc
|
||||||
|
}
|
||||||
|
return "未知筛选组"
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetFilterGroupIndex 根据中文描述获取筛选组索引
|
||||||
|
func GetFilterGroupIndex(text string) int {
|
||||||
|
// 通过遍历filterGroupMap获取对应的索引
|
||||||
|
for index, description := range filterGroupMap {
|
||||||
|
if description == text {
|
||||||
|
return index
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1 // 未找到匹配项时返回-1
|
||||||
|
}
|
||||||
|
|
||||||
type SearchAction struct {
|
type SearchAction struct {
|
||||||
page *rod.Page
|
page *rod.Page
|
||||||
}
|
}
|
||||||
@@ -26,7 +151,7 @@ func NewSearchAction(page *rod.Page) *SearchAction {
|
|||||||
return &SearchAction{page: pp}
|
return &SearchAction{page: pp}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *SearchAction) Search(ctx context.Context, keyword string) ([]Feed, error) {
|
func (s *SearchAction) Search(ctx context.Context, keyword string, filters ...FilterOption) ([]Feed, error) {
|
||||||
page := s.page.Context(ctx)
|
page := s.page.Context(ctx)
|
||||||
|
|
||||||
searchURL := makeSearchURL(keyword)
|
searchURL := makeSearchURL(keyword)
|
||||||
@@ -35,6 +160,36 @@ func (s *SearchAction) Search(ctx context.Context, keyword string) ([]Feed, erro
|
|||||||
|
|
||||||
page.MustWait(`() => window.__INITIAL_STATE__ !== undefined`)
|
page.MustWait(`() => window.__INITIAL_STATE__ !== undefined`)
|
||||||
|
|
||||||
|
// 如果有筛选条件,则应用筛选
|
||||||
|
if len(filters) > 0 {
|
||||||
|
// 验证所有筛选选项
|
||||||
|
for _, filter := range filters {
|
||||||
|
if err := validateFilterOption(filter); err != nil {
|
||||||
|
return nil, fmt.Errorf("筛选选项验证失败: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 悬停在筛选按钮上
|
||||||
|
filterButton := page.MustElement(`div.filter`)
|
||||||
|
filterButton.MustHover()
|
||||||
|
|
||||||
|
// 等待筛选面板出现
|
||||||
|
page.MustWait(`() => document.querySelector('div.filter-panel') !== null`)
|
||||||
|
|
||||||
|
// 应用所有筛选条件
|
||||||
|
for _, filter := range filters {
|
||||||
|
selector := fmt.Sprintf(`div.filter-panel div.filters:nth-child(%d) div.tags:nth-child(%d)`,
|
||||||
|
filter.FiltersIndex, filter.TagsIndex)
|
||||||
|
option := page.MustElement(selector)
|
||||||
|
option.MustClick()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 等待页面更新
|
||||||
|
page.MustWaitStable()
|
||||||
|
// 重新等待 __INITIAL_STATE__ 更新
|
||||||
|
page.MustWait(`() => window.__INITIAL_STATE__ !== undefined`)
|
||||||
|
}
|
||||||
|
|
||||||
// 获取 window.__INITIAL_STATE__ 并转换为 JSON 字符串
|
// 获取 window.__INITIAL_STATE__ 并转换为 JSON 字符串
|
||||||
result := page.MustEval(`() => {
|
result := page.MustEval(`() => {
|
||||||
if (window.__INITIAL_STATE__) {
|
if (window.__INITIAL_STATE__) {
|
||||||
@@ -61,5 +216,7 @@ func makeSearchURL(keyword string) string {
|
|||||||
values.Set("keyword", keyword)
|
values.Set("keyword", keyword)
|
||||||
values.Set("source", "web_explore_feed")
|
values.Set("source", "web_explore_feed")
|
||||||
|
|
||||||
|
//https://www.xiaohongshu.com/search_result?keyword=%25E7%258E%258B%25E5%25AD%2590&source=web_search_result_notes
|
||||||
|
//https://www.xiaohongshu.com/search_result?keyword=%25E7%258E%258B%25E5%25AD%2590&source=web_explore_feed
|
||||||
return fmt.Sprintf("https://www.xiaohongshu.com/search_result?%s", values.Encode())
|
return fmt.Sprintf("https://www.xiaohongshu.com/search_result?%s", values.Encode())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,3 +32,78 @@ func TestSearch(t *testing.T) {
|
|||||||
fmt.Printf("Feed Title: %s\n", feed.NoteCard.DisplayTitle)
|
fmt.Printf("Feed Title: %s\n", feed.NoteCard.DisplayTitle)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestSearchWithFilters(t *testing.T) {
|
||||||
|
|
||||||
|
t.Skip("SKIP: 测试筛选功能")
|
||||||
|
|
||||||
|
b := browser.NewBrowser(false)
|
||||||
|
defer b.Close()
|
||||||
|
|
||||||
|
page := b.NewPage()
|
||||||
|
defer page.Close()
|
||||||
|
|
||||||
|
action := NewSearchAction(page)
|
||||||
|
|
||||||
|
// 方式1:直接使用索引
|
||||||
|
filters1 := []FilterOption{
|
||||||
|
{FiltersIndex: 2, TagsIndex: 3, Text: "图文"}, // 笔记类型 -> 图文
|
||||||
|
{FiltersIndex: 3, TagsIndex: 2, Text: "一天内"}, // 发布时间 -> 一天内
|
||||||
|
}
|
||||||
|
|
||||||
|
feeds1, err := action.Search(context.Background(), "dn432", filters1...)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotEmpty(t, feeds1, "feeds should not be empty")
|
||||||
|
|
||||||
|
fmt.Printf("方式1 - 成功获取到 %d 个筛选后的 Feed\n", len(feeds1))
|
||||||
|
|
||||||
|
// 方式2:使用便利函数
|
||||||
|
filter2, err := NoteType("图文")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
filter3, err := TimeRange("一天内")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
filters2 := []FilterOption{filter2, filter3}
|
||||||
|
feeds2, err := action.Search(context.Background(), "dn432", filters2...)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotEmpty(t, feeds2, "feeds should not be empty")
|
||||||
|
|
||||||
|
fmt.Printf("方式2 - 成功获取到 %d 个筛选后的 Feed\n", len(feeds2))
|
||||||
|
|
||||||
|
for _, feed := range feeds2 {
|
||||||
|
fmt.Printf("Feed ID: %s\n", feed.ID)
|
||||||
|
fmt.Printf("Feed Title: %s\n", feed.NoteCard.DisplayTitle)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFilterValidation(t *testing.T) {
|
||||||
|
// 测试有效的筛选选项
|
||||||
|
validFilter := FilterOption{FiltersIndex: 2, TagsIndex: 3, Text: "图文"}
|
||||||
|
err := validateFilterOption(validFilter)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// 测试无效的筛选组索引
|
||||||
|
invalidFilterGroup := FilterOption{FiltersIndex: 6, TagsIndex: 1, Text: "无效"}
|
||||||
|
err = validateFilterOption(invalidFilterGroup)
|
||||||
|
require.Error(t, err)
|
||||||
|
require.Contains(t, err.Error(), "无效的筛选组索引")
|
||||||
|
|
||||||
|
// 测试无效的标签索引
|
||||||
|
invalidTagIndex := FilterOption{FiltersIndex: 2, TagsIndex: 5, Text: "无效"}
|
||||||
|
err = validateFilterOption(invalidTagIndex)
|
||||||
|
require.Error(t, err)
|
||||||
|
require.Contains(t, err.Error(), "标签索引 5 超出范围")
|
||||||
|
|
||||||
|
// 测试便利函数
|
||||||
|
filter, err := NoteType("图文")
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, 2, filter.FiltersIndex)
|
||||||
|
require.Equal(t, 3, filter.TagsIndex)
|
||||||
|
require.Equal(t, "图文", filter.Text)
|
||||||
|
|
||||||
|
// 测试不存在的文本
|
||||||
|
_, err = NoteType("不存在的类型")
|
||||||
|
require.Error(t, err)
|
||||||
|
require.Contains(t, err.Error(), "未找到文本")
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user