package publisher import ( "context" "fmt" "geo/internal/config" "log" "strings" "time" "github.com/go-rod/rod" "github.com/go-rod/rod/lib/proto" ) type XiaohongshuPublisher struct { *BasePublisher } // NewXiaohongshuPublisher 构造函数,增加 logger 参数 func NewXiaohongshuPublisher(ctx context.Context, task *TaskParams, cfg *config.Config, logger *log.Logger) PublisherInerface { return &XiaohongshuPublisher{NewBasePublisher(ctx, task, cfg, logger)} } func (p *XiaohongshuPublisher) WaitLogin() (bool, string) { p.LogInfo("开始等待登录...") if err := p.SetupDriver(); err != nil { return false, fmt.Sprintf("浏览器启动失败: %v", err) } defer p.Close() // 先尝试访问已登录页面 p.Page.MustNavigate(p.LoginedURL) p.Sleep(3) if p.CheckLoginStatus() { p.SaveCookies() p.LogInfo("已有登录状态") return true, "already_logged_in" } // 未登录,跳转到登录页 p.Page.MustNavigate(p.LoginURL) p.LogInfo("请扫描二维码登录...") // 等待登录完成,最多120秒 for i := 0; i < 120; i++ { time.Sleep(1 * time.Second) if p.CheckLoginStatus() { p.SaveCookies() p.LogInfo("登录成功") return true, "login_success" } p.SleepMs(1000) } return false, "登录超时" } //func (p *XiaohongshuPublisher) inputContent() error { // p.LogInfo("输入文章内容...") // // // 等待编辑器加载 // contentEditor, err := p.WaitForElementVisible(".tiptap.ProseMirror", 10) // if err != nil { // // 尝试其他选择器 // contentEditor, err = p.WaitForElementVisible("[contenteditable='true']", 10) // if err != nil { // return fmt.Errorf("未找到内容编辑器: %v", err) // } // } // // // 点击获取焦点 - 使用 Click 方法 // if err := contentEditor.Click(proto.InputMouseButtonLeft, 1); err != nil { // return fmt.Errorf("点击编辑器失败: %v", err) // } // p.SleepMs(500) // // // 清空现有内容 - 使用 JavaScript 清空 // if err := p.ClearContentEditable(contentEditor); err != nil { // p.LogInfo(fmt.Sprintf("清空编辑器失败: %v", err)) // } // p.SleepMs(300) // // // 输入新内容 - 使用 JavaScript 设置内容 // if err := p.SetContentEditable(contentEditor, p.Content); err != nil { // // 如果 JS 方式失败,尝试直接输入 // contentEditor.Input(p.Content) // } // p.LogInfo(fmt.Sprintf("内容已输入,长度: %d", len(p.Content))) // // return nil //} func (p *XiaohongshuPublisher) inputContent() error { p.LogInfo("开始导入文档内容...") // 1. 找到菜单容器并点击最后一个菜单项 menuContainer, err := p.WaitForElementVisible(".menu-items-container", 10) if err != nil { return fmt.Errorf("未找到菜单容器: %v", err) } p.LogInfo("找到菜单容器") // 获取所有菜单项 menuItems, err := menuContainer.Elements(".menu-item") if err != nil { return fmt.Errorf("未找到菜单项: %v", err) } if len(menuItems) == 0 { return fmt.Errorf("菜单容器中没有找到菜单项") } // 点击最后一个菜单项 lastMenuItem := menuItems[len(menuItems)-1] if err := p.JSClick(lastMenuItem); err != nil { return fmt.Errorf("点击最后一个菜单项失败: %v", err) } p.LogInfo("已点击最后一个菜单项") p.SleepMs(500) // 2. 等待导入文件模态框出现 var importModal *rod.Element for i := 0; i < 10; i++ { importModal, err = p.Page.Element("[class*='import-from-file-modal']") if err == nil && importModal != nil { visible, _ := importModal.Visible() if visible { p.LogInfo("找到导入文件模态框") break } } p.SleepMs(500) } if importModal == nil { return fmt.Errorf("未找到导入文件模态框") } // 查找模态框内的内容容器 modalContent, err := importModal.Element(".d-modal-content") if err != nil { return fmt.Errorf("未找到模态框内容容器: %v", err) } p.LogInfo("找到模态框内容容器") modalContent.MustClick() p.SleepMs(300) // 3. 查找隐藏的文件输入框 var fileInput *rod.Element // 尝试多种选择器 selectors := []string{ "input[type='file'][accept*='.docx']", "input[type='file'][accept*='.doc']", "input[type='file']", } for _, selector := range selectors { fileInput, err = modalContent.Element(selector) if err == nil && fileInput != nil { p.LogInfo(fmt.Sprintf("通过选择器找到文件输入框: %s", selector)) break } } if fileInput == nil { // 尝试在整个页面中查找 for _, selector := range selectors { fileInput, err = p.Page.Element(selector) if err == nil && fileInput != nil { p.LogInfo(fmt.Sprintf("在全局中找到文件输入框: %s", selector)) break } } } if fileInput == nil { return fmt.Errorf("未找到文件输入框") } // 4. 上传文件 if p.SourcePath == "" { return fmt.Errorf("源文件路径为空") } // 使用 SetFiles 方法上传文件 if err := fileInput.SetFiles([]string{p.SourcePath}); err != nil { return fmt.Errorf("上传文件失败: %v", err) } p.LogInfo(fmt.Sprintf("已选择文件: %s", p.SourcePath)) // 等待文件上传完成 p.SleepMs(2000) return nil } func (p *XiaohongshuPublisher) inputTitle() error { p.LogInfo("输入标题...") // 查找标题输入框 var titleInput *rod.Element exsist, titleInput, err := p.Page.Has("textarea[placeholder*='标题']") if exsist && err == nil { p.LogInfo(fmt.Sprintf("找到标题输入框")) } if titleInput == nil { return fmt.Errorf("未找到标题输入框") } // 点击获取焦点 if err := titleInput.Click(proto.InputMouseButtonLeft, 1); err != nil { return fmt.Errorf("点击标题框失败: %v", err) } p.SleepMs(500) // 清空输入框 if err := p.ClearInput(titleInput); err != nil { // 备用清空方式 titleInput.Input("") } p.SleepMs(300) // 输入标题 if err := p.SetInputValue(titleInput, p.Title); err != nil { // 备用输入方式 titleInput.Input(p.Title) } p.LogInfo(fmt.Sprintf("标题已输入: %s", p.Title)) return nil } func (p *XiaohongshuPublisher) inputTags() error { if len(p.Tags) == 0 { p.LogInfo("无标签需要设置") return nil } p.LogInfo(fmt.Sprintf("设置标签: %v", p.Tags)) // 构建标签字符串 tagStr := "" for _, tag := range p.Tags { if tagStr != "" { tagStr += " " } tagStr += "#" + tag } // 查找标签输入区域 tagInput, err := p.WaitForElementVisible(".tiptap-container [contenteditable='true']", 5) if err != nil { p.LogInfo("未找到标签输入框,跳过标签设置") return nil } if err := tagInput.Click(proto.InputMouseButtonLeft, 1); err != nil { p.LogInfo(fmt.Sprintf("点击标签框失败: %v", err)) } p.SleepMs(500) if err := p.SetContentEditable(tagInput, tagStr); err != nil { tagInput.Input(tagStr) } p.LogInfo("标签设置完成") return nil } func (p *XiaohongshuPublisher) uploadImage() error { if p.ImagePath == "" { p.LogInfo("无封面图片,跳过上传") return nil } p.LogInfo(fmt.Sprintf("上传封面图片: %s", p.ImagePath)) // 查找封面上传按钮 uploadBtn, err := p.WaitForElementClickable(".upload-content", 5) if err != nil { p.LogInfo("未找到封面上传区域,跳过") return nil } // 使用 Click 方法 if err := uploadBtn.Click(proto.InputMouseButtonLeft, 1); err != nil { p.LogInfo(fmt.Sprintf("点击上传按钮失败: %v", err)) } p.SleepMs(1000) // 查找文件输入框 fileInput, err := p.Page.Element("input[type='file']") if err != nil { return fmt.Errorf("未找到文件输入框: %v", err) } // 使用 SetFiles 上传文件 if err := fileInput.SetFiles([]string{p.ImagePath}); err != nil { return fmt.Errorf("上传图片失败: %v", err) } p.LogInfo("图片上传成功") p.Sleep(3) return nil } func (p *XiaohongshuPublisher) clickPublish() error { p.LogInfo("点击发布按钮...") // 滚动到底部 if _, err := p.Page.Eval(`() => window.scrollTo(0, document.body.scrollHeight)`); err != nil { p.LogInfo(fmt.Sprintf("滚动到底部失败: %v", err)) } p.SleepMs(1000) // 查找并点击 next-btn(对应Python中的第一步) for attempt := 0; attempt < p.MaxRetries; attempt++ { nextBtns, err := p.Page.Elements("button[class*='next-btn']") if err != nil || len(nextBtns) == 0 { nextBtns, err = p.Page.Elements(".next-btn") } if err == nil && len(nextBtns) > 0 { if err := p.JSClick(nextBtns[0]); err != nil { p.LogInfo(fmt.Sprintf("点击next-btn失败: %v", err)) } else { p.LogInfo("已点击next-btn") p.SleepMs(1000) } } p.SleepMs(p.RetryDelay) } // 进入发布设置页面,点击submit按钮 p.LogInfo("进入发布设置页面...") for attempt := 0; attempt < p.MaxRetries; attempt++ { submitBtn, err := p.WaitForElement("button[class*='submit']", 3) if err != nil { submitBtn, err = p.WaitForElement("button.submit", 3) } if err == nil && submitBtn != nil { if err := p.JSClick(submitBtn); err != nil { p.LogInfo(fmt.Sprintf("点击submit按钮失败: %v", err)) } else { p.LogInfo("已点击submit按钮") p.SleepMs(2000) break } } p.LogInfo(fmt.Sprintf("未找到submit按钮,第%d次重试...", attempt+1)) p.SleepMs(p.RetryDelay) } // 输入话题标签 p.LogInfo("输入话题标签...") exist, tiptap, err := p.Page.Has(".tiptap-container") if err == nil && exist { editors, err := tiptap.Elements("[contenteditable='true']") if err == nil && len(editors) > 0 { // 将tags转换为 #tag1 #tag2 格式 var tagStrings []string for _, tag := range p.Tags { if tag != "" && strings.TrimSpace(tag) != "" { tagStrings = append(tagStrings, "#"+tag) } } tagString := strings.Join(tagStrings, " ") p.LogInfo(fmt.Sprintf("输入标签: %s", tagString)) if err := p.JSInputContentEditable(editors[0], tagString); err != nil { p.LogInfo(fmt.Sprintf("输入标签失败: %v", err)) } p.SleepMs(1000) } } p.SleepMs(2000) // 最终发布 p.LogInfo("最终发布...") for attempt := 0; attempt < p.MaxRetries; attempt++ { if p.CheckElementExists(".publish-page-publish-btn", 2) { publishDiv, err := p.Page.Element(".publish-page-publish-btn") if err == nil && publishDiv != nil { buttons, err := publishDiv.Elements("button") if err == nil && len(buttons) >= 2 { if err := p.JSClick(buttons[1]); err != nil { p.LogInfo(fmt.Sprintf("点击最终发布按钮失败: %v", err)) } else { p.LogInfo("已点击最终发布按钮") break } } } } p.SleepMs(p.RetryDelay) } return nil } func (p *XiaohongshuPublisher) waitForPublishResult() (bool, string) { p.LogInfo("等待发布结果...") // 检查URL是否包含success for attempt := 0; attempt < 30; attempt++ { info, err := p.Page.Info() if err == nil && strings.Contains(info.URL, "success") { p.LogInfo(fmt.Sprintf("发布成功,URL包含success: %s", info.URL)) return true, "发布成功" } // 检查是否出现失败提示 exist, toastDiv, err := p.Page.Has(".creator-publish-toast") if err == nil && exist { toastText, err := toastDiv.Text() if err == nil && toastText != "" { p.LogInfo(fmt.Sprintf("发布失败提示: %s", toastText)) return false, fmt.Sprintf("发布失败: %s", toastText) } } p.SleepMs(1000) } return false, "发布结果未知" } // JSInputContentEditable 向contenteditable元素输入内容 func (p *XiaohongshuPublisher) JSInputContentEditable(element *rod.Element, text string) error { _, err := element.Eval(fmt.Sprintf(`() => { this.innerText = %s; }`, jsQuote(text))) return err } // CheckElementExists 检查元素是否存在 func (p *XiaohongshuPublisher) CheckElementExists(selector string, timeout int) bool { _, err := p.WaitForElement(selector, timeout) return err == nil } func jsQuote(s string) string { return "`" + strings.ReplaceAll(s, "`", "\\`") + "`" } func (p *XiaohongshuPublisher) CheckLoginStatus() bool { url := p.GetCurrentURL() // 如果URL包含登录相关关键词,表示未登录 if strings.Contains(url, p.LoginURL) { return false } return true } func (p *XiaohongshuPublisher) ClickUploadBotton() error { // 执行发布流程之前,先点击上传区域的第一个按钮 uploadDiv, err := p.WaitForElement(".upload-content", 10) if err != nil { return fmt.Errorf("未找到上传区域: %v", err) } else { buttons, err := uploadDiv.Elements("button") if err != nil { p.LogInfo(fmt.Sprintf("查找按钮失败: %v", err)) } else if len(buttons) > 0 { if err := p.JSClick(buttons[0]); err != nil { return fmt.Errorf("JS点击按钮失败: %v", err) } else { p.LogInfo("已点击上传按钮") p.Sleep(1) } } } return nil } func (p *XiaohongshuPublisher) InitPage() error { // 尝试加载cookies并检查登录状态 if err := p.LoadCookies(); err == nil { p.Page.MustNavigate(p.EditorURL) p.WaitForPageReady(5) p.Sleep(2) } // 统一检查登录状态 if !p.CheckLoginStatus() { p.LogInfo("未登录或登录已过期,需要重新登录") return fmt.Errorf("需要登录") } return nil } func (p *XiaohongshuPublisher) PublishNote() (bool, string) { p.StartNote() // 初始化浏览器 if err := p.SetupDriver(); err != nil { return false, fmt.Sprintf("浏览器启动失败: %v", err) } defer p.Close() // 执行发布流程 steps := []struct { name string fn func() error }{ {"初始化", p.InitPage}, {"保存cookie", p.SaveCookies}, {"点击上传按钮", p.ClickUploadBotton}, {"输入内容", p.inputContent}, {"输入标题", p.inputTitle}, {"点击发布", p.clickPublish}, } for _, step := range steps { if err := step.fn(); err != nil { p.LogStep(step.name, false, err.Error()) return false, fmt.Sprintf("%s失败: %v", step.name, err) } p.LogStep(step.name, true, "") p.SleepMs(500) } // 等待发布结果 return p.waitForPublishResult() }