From fcbf554016214f45cfab00da412652acb3cbe777 Mon Sep 17 00:00:00 2001 From: zy Date: Sat, 28 Feb 2026 00:37:47 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20Private=20bool=20=E2=86=92=20Visibi?= =?UTF-8?q?lity=20string=20=E6=94=AF=E6=8C=81=E5=A4=9A=E7=A7=8D=E5=8F=AF?= =?UTF-8?q?=E8=A7=81=E8=8C=83=E5=9B=B4=20(#464)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * docs: 更新 API 文档以包含 private 参数的用途和可选性。 * refactor: visibility 功能从 Private bool 重构为 Visibility string 将发布时可见范围参数从 `Private bool` 改为 `Visibility string`, 支持三种选项:公开可见(默认)、仅自己可见、仅互关好友可见。 - 使用精确 CSS selector 替代遍历 span/label/div 的宽泛选择器 - 新增参数校验,不支持的选项直接返回错误 - 更新 API 文档和 MCP jsonschema 描述 - 与 upstream IsOriginal(原创声明) 功能共存 Co-Authored-By: Claude Opus 4.6 --------- Co-authored-by: yryangang Co-authored-by: Claude Opus 4.6 --- docs/API.md | 8 +++-- mcp_handlers.go | 20 +++++++++++-- mcp_server.go | 4 +++ service.go | 4 +++ xiaohongshu/publish.go | 58 ++++++++++++++++++++++++++++++++++-- xiaohongshu/publish_video.go | 10 +++++-- 6 files changed, 95 insertions(+), 9 deletions(-) diff --git a/docs/API.md b/docs/API.md index b2f36b8..eabd086 100644 --- a/docs/API.md +++ b/docs/API.md @@ -171,7 +171,8 @@ Content-Type: application/json "http://example.com/image1.jpg", "http://example.com/image2.jpg" ], - "tags": ["标签1", "标签2"] + "tags": ["标签1", "标签2"], + "visibility": "公开可见" } ``` @@ -180,6 +181,7 @@ Content-Type: application/json - `content` (string, required): 笔记内容 - `images` (array, required): 图片URL数组,至少包含一张图片 - `tags` (array, optional): 标签数组 +- `visibility` (string, optional): 可见范围,支持: `公开可见`(默认)、`仅自己可见`、`仅互关好友可见`。不填则默认公开可见 **响应** ```json @@ -212,7 +214,8 @@ Content-Type: application/json "title": "视频标题", "content": "视频内容描述", "video": "/Users/username/Videos/video.mp4", - "tags": ["标签1", "标签2"] + "tags": ["标签1", "标签2"], + "visibility": "公开可见" } ``` @@ -221,6 +224,7 @@ Content-Type: application/json - `content` (string, required): 视频内容描述 - `video` (string, required): 本地视频文件绝对路径 - `tags` (array, optional): 标签数组 +- `visibility` (string, optional): 可见范围,支持: `公开可见`(默认)、`仅自己可见`、`仅互关好友可见`。不填则默认公开可见 **响应** ```json diff --git a/mcp_handlers.go b/mcp_handlers.go index 2cd3673..02b4703 100644 --- a/mcp_handlers.go +++ b/mcp_handlers.go @@ -15,6 +15,18 @@ import ( // MCP 工具处理函数 +// parseVisibility 从 MCP 参数中解析可见范围 +func parseVisibility(args map[string]interface{}) string { + v, ok := args["visibility"] + if !ok || v == nil { + return "" + } + if s, ok := v.(string); ok { + return s + } + return "" +} + // handleCheckLoginStatus 处理检查登录状态 func (s *AppServer) handleCheckLoginStatus(ctx context.Context) *MCPToolResult { logrus.Info("MCP: 检查登录状态") @@ -134,11 +146,12 @@ func (s *AppServer) handlePublishContent(ctx context.Context, args map[string]in // 解析定时发布参数 scheduleAt, _ := args["schedule_at"].(string) + visibility := parseVisibility(args) // 解析原创参数 isOriginal, _ := args["is_original"].(bool) - logrus.Infof("MCP: 发布内容 - 标题: %s, 图片数量: %d, 标签数量: %d, 定时: %s, 原创: %v", title, len(imagePaths), len(tags), scheduleAt, isOriginal) + logrus.Infof("MCP: 发布内容 - 标题: %s, 图片数量: %d, 标签数量: %d, 定时: %s, 原创: %v, visibility: %s", title, len(imagePaths), len(tags), scheduleAt, isOriginal, visibility) // 构建发布请求 req := &PublishRequest{ @@ -148,6 +161,7 @@ func (s *AppServer) handlePublishContent(ctx context.Context, args map[string]in Tags: tags, ScheduleAt: scheduleAt, IsOriginal: isOriginal, + Visibility: visibility, } // 执行发布 @@ -199,8 +213,9 @@ func (s *AppServer) handlePublishVideo(ctx context.Context, args map[string]inte // 解析定时发布参数 scheduleAt, _ := args["schedule_at"].(string) + visibility := parseVisibility(args) - logrus.Infof("MCP: 发布视频 - 标题: %s, 标签数量: %d, 定时: %s", title, len(tags), scheduleAt) + logrus.Infof("MCP: 发布视频 - 标题: %s, 标签数量: %d, 定时: %s, visibility: %s", title, len(tags), scheduleAt, visibility) // 构建发布请求 req := &PublishVideoRequest{ @@ -209,6 +224,7 @@ func (s *AppServer) handlePublishVideo(ctx context.Context, args map[string]inte Video: videoPath, Tags: tags, ScheduleAt: scheduleAt, + Visibility: visibility, } // 执行发布 diff --git a/mcp_server.go b/mcp_server.go index 223067f..c6466de 100644 --- a/mcp_server.go +++ b/mcp_server.go @@ -23,6 +23,7 @@ type PublishContentArgs struct { Tags []string `json:"tags,omitempty" jsonschema:"话题标签列表(可选参数),如 [美食, 旅行, 生活]"` ScheduleAt string `json:"schedule_at,omitempty" jsonschema:"定时发布时间(可选),ISO8601格式如 2024-01-20T10:30:00+08:00,支持1小时至14天内。不填则立即发布"` IsOriginal bool `json:"is_original,omitempty" jsonschema:"是否声明原创(可选),true为声明原创,false或不填则不声明"` + Visibility string `json:"visibility,omitempty" jsonschema:"可见范围(可选),支持: 公开可见(默认)、仅自己可见、仅互关好友可见。不填则默认公开可见"` } // PublishVideoArgs 发布视频的参数(仅支持本地单个视频文件) @@ -32,6 +33,7 @@ type PublishVideoArgs struct { Video string `json:"video" jsonschema:"本地视频绝对路径(仅支持单个视频文件,如:/Users/user/video.mp4)"` Tags []string `json:"tags,omitempty" jsonschema:"话题标签列表(可选参数),如 [美食, 旅行, 生活]"` ScheduleAt string `json:"schedule_at,omitempty" jsonschema:"定时发布时间(可选),ISO8601格式如 2024-01-20T10:30:00+08:00,支持1小时至14天内。不填则立即发布"` + Visibility string `json:"visibility,omitempty" jsonschema:"可见范围(可选),支持: 公开可见(默认)、仅自己可见、仅互关好友可见。不填则默认公开可见"` } // SearchFeedsArgs 搜索内容的参数 @@ -216,6 +218,7 @@ func registerTools(server *mcp.Server, appServer *AppServer) { "tags": convertStringsToInterfaces(args.Tags), "schedule_at": args.ScheduleAt, "is_original": args.IsOriginal, + "visibility": args.Visibility, } result := appServer.handlePublishContent(ctx, argsMap) return convertToMCPResult(result), nil, nil @@ -387,6 +390,7 @@ func registerTools(server *mcp.Server, appServer *AppServer) { "video": args.Video, "tags": convertStringsToInterfaces(args.Tags), "schedule_at": args.ScheduleAt, + "visibility": args.Visibility, } result := appServer.handlePublishVideo(ctx, argsMap) return convertToMCPResult(result), nil, nil diff --git a/service.go b/service.go index 16a5da1..e414bcf 100644 --- a/service.go +++ b/service.go @@ -34,6 +34,7 @@ type PublishRequest struct { Tags []string `json:"tags,omitempty"` ScheduleAt string `json:"schedule_at,omitempty"` // 定时发布时间,ISO8601格式,为空则立即发布 IsOriginal bool `json:"is_original,omitempty"` // 是否声明原创 + Visibility string `json:"visibility,omitempty"` // 可见范围: "公开可见"(默认), "仅自己可见", "仅互关好友可见" } // LoginStatusResponse 登录状态响应 @@ -65,6 +66,7 @@ type PublishVideoRequest struct { Video string `json:"video" binding:"required"` Tags []string `json:"tags,omitempty"` ScheduleAt string `json:"schedule_at,omitempty"` // 定时发布时间,ISO8601格式,为空则立即发布 + Visibility string `json:"visibility,omitempty"` // 可见范围: "公开可见"(默认), "仅自己可见", "仅互关好友可见" } // PublishVideoResponse 发布视频响应 @@ -214,6 +216,7 @@ func (s *XiaohongshuService) PublishContent(ctx context.Context, req *PublishReq ImagePaths: imagePaths, ScheduleTime: scheduleTime, IsOriginal: req.IsOriginal, + Visibility: req.Visibility, } // 执行发布 @@ -303,6 +306,7 @@ func (s *XiaohongshuService) PublishVideo(ctx context.Context, req *PublishVideo Tags: req.Tags, VideoPath: req.Video, ScheduleTime: scheduleTime, + Visibility: req.Visibility, } // 执行发布 diff --git a/xiaohongshu/publish.go b/xiaohongshu/publish.go index 3cdf212..79480bb 100644 --- a/xiaohongshu/publish.go +++ b/xiaohongshu/publish.go @@ -23,6 +23,7 @@ type PublishImageContent struct { ImagePaths []string ScheduleTime *time.Time // 定时发布时间,nil 表示立即发布 IsOriginal bool // 是否声明原创 + Visibility string // 可见范围: "公开可见"(默认), "仅自己可见", "仅互关好友可见" } type PublishAction struct { @@ -83,9 +84,9 @@ func (p *PublishAction) Publish(ctx context.Context, content PublishImageContent tags = tags[:10] } - logrus.Infof("发布内容: title=%s, images=%v, tags=%v, schedule=%v, original=%v", content.Title, len(content.ImagePaths), tags, content.ScheduleTime, content.IsOriginal) + logrus.Infof("发布内容: title=%s, images=%v, tags=%v, schedule=%v, original=%v, visibility=%s", content.Title, len(content.ImagePaths), tags, content.ScheduleTime, content.IsOriginal, content.Visibility) - if err := submitPublish(page, content.Title, content.Content, tags, content.ScheduleTime, content.IsOriginal); err != nil { + if err := submitPublish(page, content.Title, content.Content, tags, content.ScheduleTime, content.IsOriginal, content.Visibility); err != nil { return errors.Wrap(err, "小红书发布失败") } @@ -269,7 +270,7 @@ func waitForUploadComplete(page *rod.Page, expectedCount int) error { return errors.Errorf("第%d张图片上传超时(60s),请检查网络连接和图片大小", expectedCount) } -func submitPublish(page *rod.Page, title, content string, tags []string, scheduleTime *time.Time, isOriginal bool) error { +func submitPublish(page *rod.Page, title, content string, tags []string, scheduleTime *time.Time, isOriginal bool, visibility string) error { titleElem, err := page.Element("div.d-input input") if err != nil { return errors.Wrap(err, "查找标题输入框失败") @@ -314,6 +315,11 @@ func submitPublish(page *rod.Page, title, content string, tags []string, schedul slog.Info("定时发布设置完成", "schedule_time", scheduleTime.Format("2006-01-02 15:04")) } + // 设置可见范围 + if err := setVisibility(page, visibility); err != nil { + return errors.Wrap(err, "设置可见范围失败") + } + // 处理原创声明 if isOriginal { if err := setOriginal(page); err != nil { @@ -572,6 +578,52 @@ func isElementVisible(elem *rod.Element) bool { return visible } +// setVisibility 设置可见范围 +// 支持: "公开可见"(默认), "仅自己可见", "仅互关好友可见" +func setVisibility(page *rod.Page, visibility string) error { + if visibility == "" || visibility == "公开可见" { + slog.Info("可见范围使用默认:公开可见") + return nil + } + + // 支持的选项校验 + supported := map[string]bool{"仅自己可见": true, "仅互关好友可见": true} + if !supported[visibility] { + return errors.Errorf("不支持的可见范围: %s,支持: 公开可见、仅自己可见、仅互关好友可见", visibility) + } + + // 点击可见范围下拉框 + dropdown, err := page.Element("div.permission-card-wrapper div.d-select-content") + if err != nil { + return errors.Wrap(err, "查找可见范围下拉框失败") + } + if err := dropdown.Click(proto.InputMouseButtonLeft, 1); err != nil { + return errors.Wrap(err, "点击可见范围下拉框失败") + } + time.Sleep(500 * time.Millisecond) + + // 在弹窗中查找并点击目标选项 + opts, err := page.Elements("div.d-options-wrapper div.d-grid-item div.custom-option") + if err != nil { + return errors.Wrap(err, "查找可见范围选项失败") + } + for _, opt := range opts { + text, err := opt.Text() + if err != nil { + continue + } + if strings.Contains(text, visibility) { + if err := opt.Click(proto.InputMouseButtonLeft, 1); err != nil { + return errors.Wrap(err, "选择可见范围失败") + } + slog.Info("已设置可见范围", "visibility", visibility) + time.Sleep(200 * time.Millisecond) + return nil + } + } + return errors.Errorf("未找到可见范围选项: %s", visibility) +} + // setSchedulePublish 设置定时发布时间 func setSchedulePublish(page *rod.Page, t time.Time) error { // 1. 点击定时发布开关 diff --git a/xiaohongshu/publish_video.go b/xiaohongshu/publish_video.go index 5ea7263..e317922 100644 --- a/xiaohongshu/publish_video.go +++ b/xiaohongshu/publish_video.go @@ -20,6 +20,7 @@ type PublishVideoContent struct { Tags []string VideoPath string ScheduleTime *time.Time // 定时发布时间,nil 表示立即发布 + Visibility string // 可见范围: "公开可见"(默认), "仅自己可见", "仅互关好友可见" } // NewPublishVideoAction 进入发布页并切换到"上传视频" @@ -62,7 +63,7 @@ func (p *PublishAction) PublishVideo(ctx context.Context, content PublishVideoCo return errors.Wrap(err, "小红书上传视频失败") } - if err := submitPublishVideo(page, content.Title, content.Content, content.Tags, content.ScheduleTime); err != nil { + if err := submitPublishVideo(page, content.Title, content.Content, content.Tags, content.ScheduleTime, content.Visibility); err != nil { return errors.Wrap(err, "小红书发布失败") } return nil @@ -130,7 +131,7 @@ func waitForPublishButtonClickable(page *rod.Page) (*rod.Element, error) { } // submitPublishVideo 填写标题、正文、标签并点击发布(等待按钮可点击后再提交) -func submitPublishVideo(page *rod.Page, title, content string, tags []string, scheduleTime *time.Time) error { +func submitPublishVideo(page *rod.Page, title, content string, tags []string, scheduleTime *time.Time, visibility string) error { // 标题 titleElem, err := page.Element("div.d-input input") if err != nil { @@ -163,6 +164,11 @@ func submitPublishVideo(page *rod.Page, title, content string, tags []string, sc slog.Info("定时发布设置完成", "schedule_time", scheduleTime.Format("2006-01-02 15:04")) } + // 设置可见范围 + if err := setVisibility(page, visibility); err != nil { + return errors.Wrap(err, "设置可见范围失败") + } + // 等待发布按钮可点击 btn, err := waitForPublishButtonClickable(page) if err != nil {