refactor: Private bool → Visibility string 支持多种可见范围 (#464)
* 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 <noreply@anthropic.com> --------- Co-authored-by: yryangang <dd101bb@qq.com> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
// 执行发布
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
// 执行发布
|
||||
|
||||
@@ -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. 点击定时发布开关
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user