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 DouyinSpPublisher struct { *BasePublisher } func NewDouyinSpPublisher(ctx context.Context, task *TaskParams, cfg *config.Config, logger *log.Logger) PublisherInerface { return &DouyinSpPublisher{NewBasePublisher(ctx, task, cfg, logger)} } func (p *DouyinSpPublisher) CheckLogin() (bool, string) { p.LogInfo("检查登录状态...") if err := p.SetupDriver(); err != nil { return false, fmt.Sprintf("浏览器启动失败: %v", err) } defer p.Close() p.Page.MustNavigate(p.EditorURL) p.Sleep(3) p.WaitForPageReady(5) if p.CheckLoginStatus() { p.SaveCookies() return true, "已登录" } return false, "未登录" } func (p *DouyinSpPublisher) CheckLoginStatus() bool { currentURL := p.GetCurrentURL() if strings.Contains(currentURL, p.LoginedURL) { return true } if strings.Contains(currentURL, p.EditorURL) { return true } return false } func (p *DouyinSpPublisher) WaitLogin() (bool, string) { p.LogInfo("开始等待登录...") if err := p.SetupDriver(); err != nil { return false, fmt.Sprintf("浏览器启动失败: %v", err) } defer p.Close() p.Page.MustNavigate(p.LoginURL) p.Sleep(2) 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 *DouyinSpPublisher) waitForEditorReady(timeout int) bool { p.LogInfo("等待编辑器加载...") startTime := time.Now() for time.Since(startTime) < time.Duration(timeout)*time.Second { uploadArea, err := p.Page.Element(".container-drag-icon") if err == nil && uploadArea != nil { p.LogInfo("编辑器加载完成") return true } p.SleepMs(1000) } p.LogInfo("编辑器加载超时") return false } func (p *DouyinSpPublisher) 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']", ".container-drag-VAfIfu input[type='file']", "input[accept*='video']", } var fileInput *rod.Element for _, selector := range fileInputSelectors { fileInput, _ = p.Page.Element(selector) if fileInput != nil { p.LogInfo(fmt.Sprintf("找到文件上传输入框: %s", selector)) break } } if fileInput == nil { uploadArea, err := p.WaitForElementVisible(".container-drag-VAfIfu", 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 *DouyinSpPublisher) waitForUploadComplete() error { p.LogInfo("等待视频上传完成...") for i := 0; i < 300; i++ { successElements, _ := p.Page.Elements(".upload-success, [class*='success']") if len(successElements) > 0 { p.LogInfo("视频上传成功") p.SleepMs(2000) return nil } errorElements, _ := p.Page.Elements(".upload-error, [class*='error']") for _, elem := range errorElements { visible, _ := elem.Visible() if visible { text, _ := elem.Text() if text != "" { return fmt.Errorf("上传失败: %s", text) } } } p.SleepMs(1000) } return fmt.Errorf("上传超时") } func (p *DouyinSpPublisher) inputTitle() error { p.LogInfo("输入视频标题...") titleSelectors := []string{ "textarea[placeholder*='标题']", "input[placeholder*='标题']", ".container-sGoJ9f input", ".semiInput-EyEyPL input", } for _, selector := range titleSelectors { titleInput, err := p.WaitForElementVisible(selector, 5) if err == nil && titleInput != nil { p.LogInfo(fmt.Sprintf("找到标题输入框: %s", selector)) 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 } } return fmt.Errorf("未找到标题输入框") } func (p *DouyinSpPublisher) inputDescription() error { p.LogInfo("输入视频描述...") descSelectors := []string{ ".editor-kit-container", ".ProseMirror", "[contenteditable='true']", } for _, selector := range descSelectors { descInput, err := p.WaitForElementVisible(selector, 5) if err == nil && descInput != nil { p.LogInfo(fmt.Sprintf("找到描述输入框: %s", selector)) fullDescription := "" for _, tag := range p.Tags { fullDescription += fmt.Sprintf("#%s ", tag) } fullDescription = strings.TrimSpace(fullDescription) p.SetContentEditable(descInput, fullDescription) p.SleepMs(3000) return nil } } return fmt.Errorf("未找到描述输入框") } func (p *DouyinSpPublisher) clickPublish() error { p.LogInfo("点击发布按钮...") p.Page.Eval(`() => window.scrollTo(0, document.body.scrollHeight)`) p.SleepMs(2000) var publishBtn *rod.Element popoverSpan, err := p.WaitForElementVisible("#popover-tip-container", 5) if err == nil && popoverSpan != nil { publishBtn, _ = popoverSpan.Element("button") if publishBtn != nil { text, _ := publishBtn.Text() if text == "发布" { p.LogInfo("通过 popover-tip-container 找到发布按钮") } } } if publishBtn == nil { publishSelectors := []string{ "button:contains('发布')", "button.primary-cECiOJ", "button.primary_button", "button[class*='primary']", } for _, selector := range publishSelectors { publishBtn, _ = p.WaitForElementClickable(selector, 3) if publishBtn != nil { p.LogInfo(fmt.Sprintf("找到发布按钮: %s", selector)) break } } } if publishBtn == nil { p.Page.Eval(`() => window.scrollTo(0, document.body.scrollHeight)`) p.SleepMs(1000) allButtons, _ := p.Page.Elements("button") for _, btn := range allButtons { text, _ := btn.Text() if text == "发布" { publishBtn = btn p.LogInfo("通过遍历按钮找到发布按钮") break } } } if publishBtn == nil { return fmt.Errorf("未找到发布按钮") } p.SleepMs(500) if err := publishBtn.Click(proto.InputMouseButtonLeft, 1); err != nil { if err := p.JSClick(publishBtn); err != nil { return fmt.Errorf("点击发布按钮失败: %v", err) } } p.LogInfo("已点击发布按钮") p.SleepMs(3000) confirmSelectors := []string{ ".semi-modal .semi-button-primary", ".confirm-btn-JwJNCk", "button:contains('确认')", "button:contains('确定')", } for _, selector := range confirmSelectors { confirmBtn, _ := p.WaitForElementClickable(selector, 2) if confirmBtn != nil { p.JSClick(confirmBtn) p.LogInfo("已确认发布") p.SleepMs(2000) break } } return nil } func (p *DouyinSpPublisher) waitForPublishResult(timeout int) (bool, string) { p.LogInfo("等待发布结果...") startTime := time.Now() for time.Since(startTime) < time.Duration(timeout)*time.Second { currentURL := p.GetCurrentURL() successKeywords := []string{"content/manage", "work-management", "success"} for _, keyword := range successKeywords { if strings.Contains(currentURL, keyword) { p.LogInfo(fmt.Sprintf("发布成功!URL: %s", currentURL)) return true, "发布成功" } } successSelectors := []string{".semi-toast-content", ".toast-success", "[class*='success']"} for _, selector := range successSelectors { msgs, _ := p.Page.Elements(selector) for _, elem := range msgs { visible, _ := elem.Visible() if visible { text, _ := elem.Text() if strings.Contains(text, "成功") || strings.Contains(text, "已发布") { p.LogInfo(fmt.Sprintf("发布成功: %s", text)) return true, text } } } } errorSelectors := []string{".semi-toast-content", ".toast-error", "[class*='error']"} for _, selector := range errorSelectors { msgs, _ := p.Page.Elements(selector) for _, elem := range msgs { visible, _ := elem.Visible() if visible { text, _ := elem.Text() if strings.Contains(text, "失败") || strings.Contains(strings.ToLower(text), "error") { p.LogError(fmt.Sprintf("发布失败: %s", text)) return false, text } } } } p.SleepMs(1000) } return false, "发布结果未知(超时)" } // InitPage 初始化页面 func (p *DouyinSpPublisher) 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("需要登录") } p.SaveCookies() return nil } func (p *DouyinSpPublisher) PublishNote() (bool, string) { p.StartNote() if err := p.SetupDriver(); err != nil { return false, fmt.Sprintf("浏览器启动失败: %v", err) } defer p.Page.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, "") } success, message := p.waitForPublishResult(120) if success { p.LogInfo(fmt.Sprintf("🎉 发布成功!%s", message)) return true, message } p.LogError(fmt.Sprintf("发布失败: %s", message)) return false, message } func (p *DouyinSpPublisher) LogWarning(message string) { p.Logger.Printf("⚠️ %s", message) }