From c22e8758ed6b0ac47d7f0004253840f2c386e1db Mon Sep 17 00:00:00 2001 From: liangzx Date: Fri, 27 Feb 2026 17:03:38 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E5=B0=8F=E7=BA=A2?= =?UTF-8?q?=E4=B9=A6=E5=8F=91=E5=B8=83=E5=8E=9F=E5=88=9B=E5=A3=B0=E6=98=8E?= =?UTF-8?q?=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- mcp_handlers.go | 6 +- mcp_server.go | 2 + service.go | 2 + xiaohongshu/publish.go | 163 ++++++++++++++++++++++++++++++++++++++++- 4 files changed, 169 insertions(+), 4 deletions(-) diff --git a/mcp_handlers.go b/mcp_handlers.go index 2ce9e8b..2cd3673 100644 --- a/mcp_handlers.go +++ b/mcp_handlers.go @@ -135,7 +135,10 @@ func (s *AppServer) handlePublishContent(ctx context.Context, args map[string]in // 解析定时发布参数 scheduleAt, _ := args["schedule_at"].(string) - logrus.Infof("MCP: 发布内容 - 标题: %s, 图片数量: %d, 标签数量: %d, 定时: %s", title, len(imagePaths), len(tags), scheduleAt) + // 解析原创参数 + isOriginal, _ := args["is_original"].(bool) + + logrus.Infof("MCP: 发布内容 - 标题: %s, 图片数量: %d, 标签数量: %d, 定时: %s, 原创: %v", title, len(imagePaths), len(tags), scheduleAt, isOriginal) // 构建发布请求 req := &PublishRequest{ @@ -144,6 +147,7 @@ func (s *AppServer) handlePublishContent(ctx context.Context, args map[string]in Images: imagePaths, Tags: tags, ScheduleAt: scheduleAt, + IsOriginal: isOriginal, } // 执行发布 diff --git a/mcp_server.go b/mcp_server.go index 4c40607..223067f 100644 --- a/mcp_server.go +++ b/mcp_server.go @@ -22,6 +22,7 @@ type PublishContentArgs struct { Images []string `json:"images" jsonschema:"图片路径列表(至少需要1张图片)。支持两种方式:1. HTTP/HTTPS图片链接(自动下载);2. 本地图片绝对路径(推荐,如:/Users/user/image.jpg)"` 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或不填则不声明"` } // PublishVideoArgs 发布视频的参数(仅支持本地单个视频文件) @@ -214,6 +215,7 @@ func registerTools(server *mcp.Server, appServer *AppServer) { "images": convertStringsToInterfaces(args.Images), "tags": convertStringsToInterfaces(args.Tags), "schedule_at": args.ScheduleAt, + "is_original": args.IsOriginal, } result := appServer.handlePublishContent(ctx, argsMap) return convertToMCPResult(result), nil, nil diff --git a/service.go b/service.go index d082212..16a5da1 100644 --- a/service.go +++ b/service.go @@ -33,6 +33,7 @@ type PublishRequest struct { Images []string `json:"images" binding:"required,min=1"` Tags []string `json:"tags,omitempty"` ScheduleAt string `json:"schedule_at,omitempty"` // 定时发布时间,ISO8601格式,为空则立即发布 + IsOriginal bool `json:"is_original,omitempty"` // 是否声明原创 } // LoginStatusResponse 登录状态响应 @@ -212,6 +213,7 @@ func (s *XiaohongshuService) PublishContent(ctx context.Context, req *PublishReq Tags: req.Tags, ImagePaths: imagePaths, ScheduleTime: scheduleTime, + IsOriginal: req.IsOriginal, } // 执行发布 diff --git a/xiaohongshu/publish.go b/xiaohongshu/publish.go index 8f6b20e..3cdf212 100644 --- a/xiaohongshu/publish.go +++ b/xiaohongshu/publish.go @@ -22,6 +22,7 @@ type PublishImageContent struct { Tags []string ImagePaths []string ScheduleTime *time.Time // 定时发布时间,nil 表示立即发布 + IsOriginal bool // 是否声明原创 } type PublishAction struct { @@ -82,9 +83,9 @@ func (p *PublishAction) Publish(ctx context.Context, content PublishImageContent tags = tags[:10] } - logrus.Infof("发布内容: title=%s, images=%v, tags=%v, schedule=%v", content.Title, len(content.ImagePaths), tags, content.ScheduleTime) + logrus.Infof("发布内容: title=%s, images=%v, tags=%v, schedule=%v, original=%v", content.Title, len(content.ImagePaths), tags, content.ScheduleTime, content.IsOriginal) - if err := submitPublish(page, content.Title, content.Content, tags, content.ScheduleTime); err != nil { + if err := submitPublish(page, content.Title, content.Content, tags, content.ScheduleTime, content.IsOriginal); err != nil { return errors.Wrap(err, "小红书发布失败") } @@ -268,7 +269,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) error { +func submitPublish(page *rod.Page, title, content string, tags []string, scheduleTime *time.Time, isOriginal bool) error { titleElem, err := page.Element("div.d-input input") if err != nil { return errors.Wrap(err, "查找标题输入框失败") @@ -313,6 +314,15 @@ func submitPublish(page *rod.Page, title, content string, tags []string, schedul slog.Info("定时发布设置完成", "schedule_time", scheduleTime.Format("2006-01-02 15:04")) } + // 处理原创声明 + if isOriginal { + if err := setOriginal(page); err != nil { + slog.Warn("设置原创声明失败,继续发布", "error", err) + } else { + slog.Info("已声明原创") + } + } + submitButton, err := page.Element(".publish-page-publish-btn button.bg-red") if err != nil { return errors.Wrap(err, "查找发布按钮失败") @@ -612,3 +622,150 @@ func setDateTime(page *rod.Page, t time.Time) error { return nil } + +// setOriginal 设置原创声明 +func setOriginal(page *rod.Page) error { + // 根据小红书创作者页面的实际结构: + // div.custom-switch-card 包含 span.has-tips 文本为"原创声明" + // 开关是 div.d-switch 组件 + + // 查找包含"原创声明"文本的 custom-switch-card + switchCards, err := page.Elements("div.custom-switch-card") + if err != nil { + return errors.Wrap(err, "查找原创声明卡片失败") + } + + for _, card := range switchCards { + text, err := card.Text() + if err != nil { + continue + } + + // 检查是否是原创声明卡片 + if !strings.Contains(text, "原创声明") { + continue + } + + // 找到原创声明卡片,查找其中的 d-switch + switchElem, err := card.Element("div.d-switch") + if err != nil { + continue + } + + // 检查开关是否已打开 + checked, err := switchElem.Eval(`() => { + const input = this.querySelector('input[type="checkbox"]'); + return input ? input.checked : false; + }`) + if err != nil { + continue + } + + if checked.Value.Bool() { + slog.Info("原创声明已开启") + return nil + } + + // 点击开关 + if err := switchElem.Click(proto.InputMouseButtonLeft, 1); err != nil { + return errors.Wrap(err, "点击原创声明开关失败") + } + + time.Sleep(500 * time.Millisecond) + + // 处理原创声明确认弹窗 + if err := confirmOriginalDeclaration(page); err != nil { + return errors.Wrap(err, "确认原创声明失败") + } + + slog.Info("已开启原创声明") + return nil + } + + return errors.New("未找到原创声明选项") +} + +// confirmOriginalDeclaration 处理原创声明确认弹窗 +func confirmOriginalDeclaration(page *rod.Page) error { + // 等待确认弹窗出现 + time.Sleep(800 * time.Millisecond) + + // 使用 JavaScript 直接处理弹窗,更可靠 + result, err := page.Eval(` + () => { + // 查找包含"原创声明须知"的 footer 区域 + const footers = document.querySelectorAll('div.footer'); + for (const footer of footers) { + // 检查是否包含原创声明相关内容 + if (!footer.textContent.includes('原创声明须知')) { + continue; + } + + // 找到 checkbox 并勾选 + const checkbox = footer.querySelector('div.d-checkbox input[type="checkbox"]'); + if (checkbox && !checkbox.checked) { + checkbox.click(); + console.log('已勾选原创声明须知 checkbox'); + } + + // 等待一下让按钮变为可用 + return 'found_footer'; + } + return 'footer_not_found'; + } + `) + if err != nil { + slog.Warn("执行查找弹窗脚本失败", "error", err) + } else if result.Value.String() == "footer_not_found" { + slog.Warn("未找到原创声明确认弹窗的 footer") + } + + time.Sleep(500 * time.Millisecond) + + // 再次使用 JavaScript 点击声明原创按钮 + result2, err := page.Eval(` + () => { + const footers = document.querySelectorAll('div.footer'); + for (const footer of footers) { + if (!footer.textContent.includes('声明原创')) { + continue; + } + + // 找到声明原创按钮 + const btn = footer.querySelector('button.custom-button'); + if (btn) { + // 检查是否禁用 + if (btn.classList.contains('disabled') || btn.disabled) { + // 尝试再次勾选 checkbox + const checkbox = footer.querySelector('div.d-checkbox input[type="checkbox"]'); + if (checkbox && !checkbox.checked) { + checkbox.click(); + } + return 'button_disabled'; + } + btn.click(); + return 'clicked'; + } + } + return 'button_not_found'; + } + `) + if err != nil { + return errors.Wrap(err, "执行点击按钮脚本失败") + } + + status := result2.Value.String() + slog.Info("原创声明确认结果", "status", status) + + if status == "button_not_found" { + return errors.New("未找到声明原创按钮") + } + if status == "button_disabled" { + return errors.New("声明原创按钮仍处于禁用状态") + } + + slog.Info("已成功点击声明原创按钮") + time.Sleep(300 * time.Millisecond) + + return nil +}