diff --git a/.github/workflows/docker-release.yml b/.github/workflows/docker-release.yml new file mode 100644 index 0000000..33932d4 --- /dev/null +++ b/.github/workflows/docker-release.yml @@ -0,0 +1,40 @@ +name: Docker Release + +on: + workflow_dispatch: + inputs: + version: + description: 'Version tag (e.g., v1.0.0)' + required: true + default: 'v1.0.0' + +permissions: + contents: read + +jobs: + docker: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to Docker Hub + uses: docker/login-action@v3 + with: + username: xpzouying + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Build and push Docker image + uses: docker/build-push-action@v5 + with: + context: . + push: true + platforms: linux/amd64,linux/arm64 + tags: | + xpzouying/xiaohongshu-mcp:${{ github.event.inputs.version }} + xpzouying/xiaohongshu-mcp:latest + cache-from: type=gha + cache-to: type=gha,mode=max \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0d23750..c1abca5 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -142,6 +142,10 @@ jobs: ./xiaohongshu-mcp-darwin-arm64 ``` + ### 🐳 Docker 镜像 + + Docker 镜像需要手动触发构建,请到 Actions 页面运行 "Docker Release" workflow。 + ### 📊 构建信息 - **Commit**: ${{ github.sha }} diff --git a/mcp_handlers.go b/mcp_handlers.go index 0b5adc7..6c7fd1e 100644 --- a/mcp_handlers.go +++ b/mcp_handlers.go @@ -132,59 +132,59 @@ func (s *AppServer) handlePublishContent(ctx context.Context, args map[string]in // handlePublishVideo 处理发布视频内容(仅本地单个视频文件) func (s *AppServer) handlePublishVideo(ctx context.Context, args map[string]interface{}) *MCPToolResult { - logrus.Info("MCP: 发布视频内容(本地)") + logrus.Info("MCP: 发布视频内容(本地)") - title, _ := args["title"].(string) - content, _ := args["content"].(string) - videoPath, _ := args["video"].(string) - tagsInterface, _ := args["tags"].([]interface{}) + title, _ := args["title"].(string) + content, _ := args["content"].(string) + videoPath, _ := args["video"].(string) + tagsInterface, _ := args["tags"].([]interface{}) - var tags []string - for _, tag := range tagsInterface { - if tagStr, ok := tag.(string); ok { - tags = append(tags, tagStr) - } - } + var tags []string + for _, tag := range tagsInterface { + if tagStr, ok := tag.(string); ok { + tags = append(tags, tagStr) + } + } - if videoPath == "" { - return &MCPToolResult{ - Content: []MCPContent{{ - Type: "text", - Text: "发布失败: 缺少本地视频文件路径", - }}, - IsError: true, - } - } + if videoPath == "" { + return &MCPToolResult{ + Content: []MCPContent{{ + Type: "text", + Text: "发布失败: 缺少本地视频文件路径", + }}, + IsError: true, + } + } - logrus.Infof("MCP: 发布视频 - 标题: %s, 标签数量: %d", title, len(tags)) + logrus.Infof("MCP: 发布视频 - 标题: %s, 标签数量: %d", title, len(tags)) - // 构建发布请求 - req := &PublishVideoRequest{ - Title: title, - Content: content, - Video: videoPath, - Tags: tags, - } + // 构建发布请求 + req := &PublishVideoRequest{ + Title: title, + Content: content, + Video: videoPath, + Tags: tags, + } - // 执行发布 - result, err := s.xiaohongshuService.PublishVideo(ctx, req) - if err != nil { - return &MCPToolResult{ - Content: []MCPContent{{ - Type: "text", - Text: "发布失败: " + err.Error(), - }}, - IsError: true, - } - } + // 执行发布 + result, err := s.xiaohongshuService.PublishVideo(ctx, req) + if err != nil { + return &MCPToolResult{ + Content: []MCPContent{{ + Type: "text", + Text: "发布失败: " + err.Error(), + }}, + IsError: true, + } + } - resultText := fmt.Sprintf("视频发布成功: %+v", result) - return &MCPToolResult{ - Content: []MCPContent{{ - Type: "text", - Text: resultText, - }}, - } + resultText := fmt.Sprintf("视频发布成功: %+v", result) + return &MCPToolResult{ + Content: []MCPContent{{ + Type: "text", + Text: resultText, + }}, + } } // handleListFeeds 处理获取Feeds列表 diff --git a/mcp_server.go b/mcp_server.go index 35ebb20..9d6895b 100644 --- a/mcp_server.go +++ b/mcp_server.go @@ -20,10 +20,10 @@ type PublishContentArgs struct { // PublishVideoArgs 发布视频的参数(仅支持本地单个视频文件) type PublishVideoArgs struct { - Title string `json:"title" jsonschema:"内容标题(小红书限制:最多20个中文字或英文单词)"` - Content string `json:"content" jsonschema:"正文内容,不包含以#开头的标签内容,所有话题标签都用tags参数来生成和提供即可"` - Video string `json:"video" jsonschema:"本地视频绝对路径(仅支持单个视频文件,如:/Users/user/video.mp4)"` - Tags []string `json:"tags,omitempty" jsonschema:"话题标签列表(可选参数),如 [美食, 旅行, 生活]"` + Title string `json:"title" jsonschema:"内容标题(小红书限制:最多20个中文字或英文单词)"` + Content string `json:"content" jsonschema:"正文内容,不包含以#开头的标签内容,所有话题标签都用tags参数来生成和提供即可"` + Video string `json:"video" jsonschema:"本地视频绝对路径(仅支持单个视频文件,如:/Users/user/video.mp4)"` + Tags []string `json:"tags,omitempty" jsonschema:"话题标签列表(可选参数),如 [美食, 旅行, 生活]"` } // SearchFeedsArgs 搜索内容的参数 diff --git a/service.go b/service.go index f25ebeb..ebc1534 100644 --- a/service.go +++ b/service.go @@ -4,7 +4,6 @@ import ( "context" "encoding/json" "fmt" - "os" "github.com/go-rod/rod" "github.com/mattn/go-runewidth" "github.com/sirupsen/logrus" @@ -14,6 +13,7 @@ import ( "github.com/xpzouying/xiaohongshu-mcp/cookies" "github.com/xpzouying/xiaohongshu-mcp/pkg/downloader" "github.com/xpzouying/xiaohongshu-mcp/xiaohongshu" + "os" "time" ) @@ -48,28 +48,28 @@ type LoginQrcodeResponse struct { // PublishResponse 发布响应 type PublishResponse struct { - Title string `json:"title"` - Content string `json:"content"` - Images int `json:"images"` - Status string `json:"status"` - PostID string `json:"post_id,omitempty"` + Title string `json:"title"` + Content string `json:"content"` + Images int `json:"images"` + Status string `json:"status"` + PostID string `json:"post_id,omitempty"` } // PublishVideoRequest 发布视频请求(仅支持本地单个视频文件) type PublishVideoRequest struct { - Title string `json:"title" binding:"required"` - Content string `json:"content" binding:"required"` - Video string `json:"video" binding:"required"` - Tags []string `json:"tags,omitempty"` + Title string `json:"title" binding:"required"` + Content string `json:"content" binding:"required"` + Video string `json:"video" binding:"required"` + Tags []string `json:"tags,omitempty"` } // PublishVideoResponse 发布视频响应 type PublishVideoResponse struct { - Title string `json:"title"` - Content string `json:"content"` - Video string `json:"video"` - Status string `json:"status"` - PostID string `json:"post_id,omitempty"` + Title string `json:"title"` + Content string `json:"content"` + Video string `json:"video"` + Status string `json:"status"` + PostID string `json:"post_id,omitempty"` } // FeedsListResponse Feeds列表响应 @@ -219,55 +219,55 @@ func (s *XiaohongshuService) publishContent(ctx context.Context, content xiaohon // PublishVideo 发布视频(本地文件) func (s *XiaohongshuService) PublishVideo(ctx context.Context, req *PublishVideoRequest) (*PublishVideoResponse, error) { - // 标题长度校验 - if titleWidth := runewidth.StringWidth(req.Title); titleWidth > 40 { - return nil, fmt.Errorf("标题长度超过限制") - } + // 标题长度校验 + if titleWidth := runewidth.StringWidth(req.Title); titleWidth > 40 { + return nil, fmt.Errorf("标题长度超过限制") + } - // 本地视频文件校验 - if req.Video == "" { - return nil, fmt.Errorf("必须提供本地视频文件") - } - if _, err := os.Stat(req.Video); err != nil { - return nil, fmt.Errorf("视频文件不存在或不可访问: %v", err) - } + // 本地视频文件校验 + if req.Video == "" { + return nil, fmt.Errorf("必须提供本地视频文件") + } + if _, err := os.Stat(req.Video); err != nil { + return nil, fmt.Errorf("视频文件不存在或不可访问: %v", err) + } - // 构建发布内容 - content := xiaohongshu.PublishVideoContent{ - Title: req.Title, - Content: req.Content, - Tags: req.Tags, - VideoPath: req.Video, - } + // 构建发布内容 + content := xiaohongshu.PublishVideoContent{ + Title: req.Title, + Content: req.Content, + Tags: req.Tags, + VideoPath: req.Video, + } - // 执行发布 - if err := s.publishVideo(ctx, content); err != nil { - return nil, err - } + // 执行发布 + if err := s.publishVideo(ctx, content); err != nil { + return nil, err + } - resp := &PublishVideoResponse{ - Title: req.Title, - Content: req.Content, - Video: req.Video, - Status: "发布完成", - } - return resp, nil + resp := &PublishVideoResponse{ + Title: req.Title, + Content: req.Content, + Video: req.Video, + Status: "发布完成", + } + return resp, nil } // publishVideo 执行视频发布 func (s *XiaohongshuService) publishVideo(ctx context.Context, content xiaohongshu.PublishVideoContent) error { - b := newBrowser() - defer b.Close() + b := newBrowser() + defer b.Close() - page := b.NewPage() - defer page.Close() + page := b.NewPage() + defer page.Close() - action, err := xiaohongshu.NewPublishVideoAction(page) - if err != nil { - return err - } + action, err := xiaohongshu.NewPublishVideoAction(page) + if err != nil { + return err + } - return action.PublishVideo(ctx, content) + return action.PublishVideo(ctx, content) } // ListFeeds 获取Feeds列表