feat: 添加小红书发布原创声明功能
This commit is contained in:
@@ -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,
|
||||
}
|
||||
|
||||
// 执行发布
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
// 执行发布
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user