feat: add manual Docker release workflow (#187)
- 新增 docker-release.yml workflow,支持手动触发 Docker 镜像构建 - 从 release.yml 中移除自动 Docker 构建,避免镜像膨胀 - 使用语义化版本号策略(如 v1.0.0) - 支持多平台构建(linux/amd64, linux/arm64) - 硬编码公开信息,只需配置 DOCKERHUB_TOKEN secret - 自动标记版本号和 latest 标签 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
40
.github/workflows/docker-release.yml
vendored
Normal file
40
.github/workflows/docker-release.yml
vendored
Normal file
@@ -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
|
||||
4
.github/workflows/release.yml
vendored
4
.github/workflows/release.yml
vendored
@@ -142,6 +142,10 @@ jobs:
|
||||
./xiaohongshu-mcp-darwin-arm64
|
||||
```
|
||||
|
||||
### 🐳 Docker 镜像
|
||||
|
||||
Docker 镜像需要手动触发构建,请到 Actions 页面运行 "Docker Release" workflow。
|
||||
|
||||
### 📊 构建信息
|
||||
|
||||
- **Commit**: ${{ github.sha }}
|
||||
|
||||
@@ -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列表
|
||||
|
||||
@@ -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 搜索内容的参数
|
||||
|
||||
106
service.go
106
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列表
|
||||
|
||||
Reference in New Issue
Block a user