package publisher import ( "context" "fmt" "geo/internal/config" "log" "os" "strings" "time" "github.com/go-rod/rod" "github.com/go-rod/rod/lib/proto" ) type XiaohongshuVideoPublisher struct { *BasePublisher shortWait int mediumWait int longWait int } func NewXiaohongshuVideoPublisher(ctx context.Context, task *TaskParams, cfg *config.Config, logger *log.Logger) PublisherInerface { return &XiaohongshuVideoPublisher{ BasePublisher: NewBasePublisher(ctx, task, cfg, logger), shortWait: 1, mediumWait: 3, longWait: 5, } } func (p *XiaohongshuVideoPublisher) 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() return true, "already_logged_in" } startTime := time.Now() timeout := 120 for time.Since(startTime) < time.Duration(timeout)*time.Second { if p.CheckLoginStatus() { p.SaveCookies() return true, "login_success" } p.SleepMs(1000) } return false, "登录超时,请检查网络或账号状态" } func (p *XiaohongshuVideoPublisher) waitForEditorReady(timeout int) bool { p.LogInfo("等待编辑器加载...") startTime := time.Now() for time.Since(startTime) < time.Duration(timeout)*time.Second { uploadArea, err := p.Page.Element(".upload-wrapper") if err == nil && uploadArea != nil { p.LogInfo("编辑器加载完成") return true } p.SleepMs(1000) } p.LogInfo("编辑器加载超时") return false } func (p *XiaohongshuVideoPublisher) uploadVideo() error { if p.SourcePath == "" { return fmt.Errorf("视频不存在") } if _, err := os.Stat(p.SourcePath); os.IsNotExist(err) { return fmt.Errorf("视频不存在") } p.LogInfo(fmt.Sprintf("开始上传视频: %s", p.SourcePath)) fileInputSelectors := []string{ "input[type='file'][accept*='video']", ".upload-input", "input[accept*='mp4']", "input[accept*='video/*']", } fileInput, err := p.Page.Element(".upload-input") if err != nil { fmt.Errorf("找到文件上传输入框") } if fileInput == nil { uploadArea, err := p.WaitForElementVisible(".video-plugin-title-action", 5) if err == nil && uploadArea != nil { p.LogInfo("点击上传区域") p.JSClick(uploadArea) p.SleepMs(1000) for _, selector := range fileInputSelectors { fileInput, _ = p.Page.Element(selector) if fileInput != nil { break } } } } if fileInput == nil { return fmt.Errorf("未找到文件上传输入框") } if err := fileInput.SetFiles([]string{p.SourcePath}); err != nil { return fmt.Errorf("上传视频失败: %v", err) } p.LogInfo(fmt.Sprintf("视频文件已选择: %s", p.SourcePath)) return p.waitForUploadComplete() } func (p *XiaohongshuVideoPublisher) waitForUploadComplete() error { p.LogInfo("等待视频上传完成...") for i := 0; i < 300; i++ { publishBtn, err := p.Page.Element(".publish-page-publish-btn button.bg-red") if err == nil && publishBtn != nil { text, _ := publishBtn.Text() if text == "发布" { p.LogInfo("发布按钮已可点击,视频上传完成") return nil } } errorElem, err := p.Page.Element("[class*='error'], .toast-error") if err == nil && errorElem != nil { visible, _ := errorElem.Visible() if visible { text, _ := errorElem.Text() if text != "" { return fmt.Errorf("上传失败: %s", text) } } } p.SleepMs(1000) } return fmt.Errorf("上传超时") } func (p *XiaohongshuVideoPublisher) inputTitle() error { p.LogInfo(fmt.Sprintf("输入视频标题: %s", p.Title)) exist, titleInput, err := p.Page.Has("input[placeholder*='标题']") if err != nil { return err } if !exist { return fmt.Errorf("未找到标题输入框") } p.ClearInput(titleInput) p.SleepMs(300) titleInput.Input("") p.SleepMs(300) titleInput.Input(p.Title) p.LogInfo(fmt.Sprintf("标题已输入: %s", p.Title)) p.SleepMs(500) return nil } func (p *XiaohongshuVideoPublisher) inputDescription() error { p.LogInfo("输入视频描述...") fullDescription := p.Content if len(p.Tags) > 0 { tagStr := "" for _, tag := range p.Tags { if tag != "" { tagStr += fmt.Sprintf("#%s ", tag) } } tagStr = strings.TrimSpace(tagStr) if fullDescription != "" { fullDescription = fmt.Sprintf("%s\n\n%s", tagStr, fullDescription) } else { fullDescription = tagStr } } p.LogInfo(fmt.Sprintf("描述内容: %s...", fullDescription[:min(len(fullDescription), 100)])) editorSelectors := []string{ ".tiptap.ProseMirror", ".ProseMirror", "[contenteditable='true']", ".editor-content .tiptap", } for _, selector := range editorSelectors { editor, err := p.WaitForElementVisible(selector, 5) if err == nil && editor != nil { p.LogInfo(fmt.Sprintf("找到编辑器: %s", selector)) editor.Click(proto.InputMouseButtonLeft, 1) p.SleepMs(500) p.SetContentEditable(editor, fullDescription) p.SleepMs(1000) return nil } } return fmt.Errorf("未找到描述输入框") } func (p *XiaohongshuVideoPublisher) clickPublish() error { p.LogInfo("点击发布按钮...") p.Page.Eval(`() => window.scrollTo(0, document.body.scrollHeight)`) p.SleepMs(1000) var publishBtn *rod.Element publishContainer, err := p.WaitForElementVisible(".publish-page-publish-btn", 5) if err == nil && publishContainer != nil { buttons, _ := publishContainer.Elements("button") for _, btn := range buttons { text, _ := btn.Text() if text == "发布" { publishBtn = btn p.LogInfo("找到发布按钮") break } } } if publishBtn == nil { allButtons, _ := p.Page.Elements("button") for _, btn := range allButtons { text, _ := btn.Text() if text == "发布" && !strings.Contains(text, "暂存") { publishBtn = btn p.LogInfo("通过遍历找到发布按钮") break } } } if publishBtn == nil { return fmt.Errorf("未找到发布按钮") } publishBtn.ScrollIntoView() p.SleepMs(500) if err := publishBtn.Click(proto.InputMouseButtonLeft, 1); err != nil { p.JSClick(publishBtn) p.LogInfo("使用JS点击发布按钮") } else { p.LogInfo("已点击发布按钮") } p.SleepMs(2000) return nil } func (p *XiaohongshuVideoPublisher) waitForPublishResult(timeout int) (bool, string) { p.LogInfo("等待发布结果...") startTime := time.Now() for time.Since(startTime) < time.Duration(timeout)*time.Second { currentURL := p.GetCurrentURL() successKeywords := []string{"success", "content/manage", "work-management"} for _, keyword := range successKeywords { if strings.Contains(currentURL, keyword) { p.LogInfo(fmt.Sprintf("发布成功!URL: %s", currentURL)) return true, "发布成功" } } toasts, _ := p.Page.Elements(".semi-toast-content, [class*='toast']") for _, toast := range toasts { visible, _ := toast.Visible() if visible { text, _ := toast.Text() if strings.Contains(text, "成功") || strings.Contains(text, "已发布") { p.LogInfo(fmt.Sprintf("发布成功: %s", text)) return true, text } else if strings.Contains(text, "失败") { p.LogError(fmt.Sprintf("发布失败: %s", text)) return false, text } } } if strings.Contains(currentURL, "publish") && time.Since(startTime) > 10*time.Second { errorMsgs, _ := p.Page.Elements("[class*='error'], .toast-error") for _, elem := range errorMsgs { visible, _ := elem.Visible() if visible { text, _ := elem.Text() if text != "" { return false, text } } } } p.SleepMs(2000) } return false, "发布结果未知(超时)" } func (p *XiaohongshuVideoPublisher) 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}, {"上传视频", p.uploadVideo}, {"输入标题", p.inputTitle}, {"输入描述", p.inputDescription}, {"点击发布", 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(1000) } success, message := p.waitForPublishResult(60) if success { p.LogInfo(fmt.Sprintf("🎉 发布成功!%s", message)) return true, message } p.LogError(fmt.Sprintf("发布失败: %s", message)) return false, message }