package publisher import ( "context" "fmt" "geo/internal/config" "log" "strings" "time" "github.com/go-rod/rod" "github.com/go-rod/rod/lib/proto" ) type SohuPublisher struct { *BasePublisher } func NewSohuPublisher(ctx context.Context, task *TaskParams, cfg *config.Config, logger *log.Logger) PublisherInerface { return &SohuPublisher{NewBasePublisher(ctx, task, cfg, logger)} } func (p *SohuPublisher) 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 *SohuPublisher) CheckLoginStatus() bool { currentURL := p.GetCurrentURL() if strings.Contains(currentURL, p.LoginURL) { return false } if strings.Contains(currentURL, "clientAuth") { return false } return true } func (p *SohuPublisher) WaitLogin() (bool, string) { p.LogInfo("开始等待登录...") if err := p.SetupDriver(); err != nil { return false, fmt.Sprintf("浏览器启动失败: %v", err) } defer p.Close() if p.LoginedURL != "" { p.Page.MustNavigate(p.LoginedURL) p.Sleep(3) if p.CheckLoginStatus() { p.SaveCookies() return true, "already_logged_in" } } p.Page.MustNavigate(p.LoginURL) p.LogInfo("请扫描二维码登录...") for i := 0; i < 120; i++ { p.SleepMs(1000) if p.CheckLoginStatus() { p.SaveCookies() p.LogInfo("登录成功") return true, "login_success" } } return false, "登录超时,请检查网络或账号状态" } func (p *SohuPublisher) waitForEditorReady(timeout int) bool { p.LogInfo("等待编辑器加载...") startTime := time.Now() for time.Since(startTime) < time.Duration(timeout)*time.Second { titleSelectors := []string{ ".publish-title input", "input[placeholder*='标题']", ".article-title input", "[class*='title'] input", } for _, selector := range titleSelectors { el, err := p.Page.Element(selector) if err == nil && el != nil { visible, _ := el.Visible() if visible { p.LogInfo("编辑器加载完成") return true } } } p.SleepMs(1000) } p.LogInfo("编辑器加载超时") return false } func (p *SohuPublisher) inputTitle() error { p.LogInfo("输入文章标题...") titleSelectors := []string{ ".publish-title input", "input[placeholder*='请输入标题']", "input[placeholder*='标题']", ".article-title-input input", ".title-input input", } for _, selector := range titleSelectors { titleInput, err := p.WaitForElementVisible(selector, 5) if err == nil && titleInput != nil { p.LogInfo(fmt.Sprintf("找到标题输入框: %s", selector)) if err := p.ClearInput(titleInput); err != nil { titleInput.Input("") } p.SleepMs(300) titleInput.Input(p.Title) p.SleepMs(300) p.LogInfo(fmt.Sprintf("标题已输入: %s", p.Title)) _, err = titleInput.Evaluate(&rod.EvalOptions{ JS: `(el) => { el.dispatchEvent(new Event('input', {bubbles: true})); el.dispatchEvent(new Event('change', {bubbles: true})); el.dispatchEvent(new Event('blur', {bubbles: true})); }`, }) p.SleepMs(500) return nil } } return fmt.Errorf("未找到标题输入框") } func (p *SohuPublisher) inputContent() error { p.LogInfo("输入文章正文...") if p.Content == "" { p.LogInfo("内容为空") return fmt.Errorf("内容为空") } editorSelectors := []string{ ".ql-editor", "[contenteditable='true']", ".rich-editor-content", ".editor-content", } var editor *rod.Element var err error for _, selector := range editorSelectors { editor, err = p.WaitForElementVisible(selector, 10) if err == nil && editor != nil { p.LogInfo(fmt.Sprintf("找到编辑器: %s", selector)) break } } if editor == nil { return fmt.Errorf("未找到编辑器") } // 清空编辑器 //_, err = editor.Evaluate(&rod.EvalOptions{JS: `el => { el.innerHTML = ''; }`}) p.SleepMs(500) // 点击编辑器获取焦点 p.JSClick(editor) p.SleepMs(500) // 输入内容 err = editor.Input(p.Content) if err != nil { return fmt.Errorf("输入内容失败: %v", err) } p.LogInfo(fmt.Sprintf("内容已输入,长度: %d", len(p.Content))) return nil } func (p *SohuPublisher) setCover() error { p.LogInfo("设置封面...") if p.ImagePath == "" { p.LogInfo("未提供封面图片,尝试使用自动封面") return p.setAutoCover() } p.LogInfo(fmt.Sprintf("使用封面图片: %s", p.ImagePath)) coverSelectors := []string{ ".cover-button .upload-file", ".upload-file", "[class*='cover'] .upload-file", ".mp-upload", } var uploadBtn *rod.Element var err error for _, selector := range coverSelectors { uploadBtn, err = p.WaitForElementClickable(selector, 5) if err == nil && uploadBtn != nil { p.LogInfo(fmt.Sprintf("找到上传封面按钮: %s", selector)) break } } if uploadBtn == nil { p.LogInfo("未找到上传封面按钮") return p.setAutoCover() } if _, err := uploadBtn.Evaluate(&rod.EvalOptions{ JS: `el => el.scrollIntoView({block: 'center'})`, }); err != nil { p.LogInfo(fmt.Sprintf("滚动到按钮失败: %v", err)) } p.SleepMs(500) fileInputSelectors := []string{ "input[type='file'][accept*='image']", ".mp-upload input[type='file']", ".cover-button input[type='file']", } var fileInput *rod.Element for _, selector := range fileInputSelectors { fileInput, err = p.Page.Element(selector) if err == nil && fileInput != nil { p.LogInfo(fmt.Sprintf("找到文件上传输入框: %s", selector)) break } } if fileInput == nil { p.LogInfo("未找到文件上传输入框") return p.setAutoCover() } if err := fileInput.SetFiles([]string{p.ImagePath}); err != nil { p.LogInfo(fmt.Sprintf("上传图片失败: %v", err)) return p.setAutoCover() } p.LogInfo(fmt.Sprintf("封面图片已上传: %s", p.ImagePath)) p.Sleep(3) cropConfirm, err := p.WaitForElement(".crop-btn .sure-btn, .confirm-btn", 5) if err == nil && cropConfirm != nil { if err := p.JSClick(cropConfirm); err == nil { p.LogInfo("已确认裁剪") } p.SleepMs(1000) } return nil } func (p *SohuPublisher) setAutoCover() error { p.LogInfo("设置为自动封面...") autoCoverSelectors := []string{ "span:contains('自动')", ".cover-title .auto-cover", "[class*='auto']", } for _, selector := range autoCoverSelectors { autoElem, err := p.WaitForElement(selector, 3) if err == nil && autoElem != nil { if err := p.JSClick(autoElem); err == nil { p.LogInfo("已选择自动封面") p.SleepMs(500) return nil } } } p.LogInfo("未找到自动封面选项,跳过封面设置") return nil } func (p *SohuPublisher) setAbstract() error { p.LogInfo("设置摘要...") abstractSelectors := []string{ ".abstract-main textarea", "textarea[placeholder*='摘要']", ".abstract textarea", } var abstractInput *rod.Element var err error for _, selector := range abstractSelectors { abstractInput, err = p.WaitForElementVisible(selector, 3) if err == nil && abstractInput != nil { p.LogInfo(fmt.Sprintf("找到摘要输入框: %s", selector)) break } } if abstractInput != nil { if err := abstractInput.SelectAllText(); err == nil { abstractInput.Input("") } p.SleepMs(300) abstractText := p.Content if len(abstractText) > 120 { abstractText = abstractText[:120] } abstractInput.Input(abstractText) p.LogInfo("摘要已设置") return nil } p.LogInfo("未找到摘要输入框,跳过") return nil } func (p *SohuPublisher) setTags() error { if len(p.Tags) == 0 { p.LogInfo("无标签需要设置") return nil } p.LogInfo(fmt.Sprintf("设置标签: %v", p.Tags)) tagInputSelectors := []string{ "input[placeholder*='标签']", ".tag-input input", "[class*='tag'] input", } var tagInput *rod.Element var err error for _, selector := range tagInputSelectors { tagInput, err = p.WaitForElement(selector, 3) if err == nil && tagInput != nil { p.LogInfo(fmt.Sprintf("找到标签输入框: %s", selector)) break } } if tagInput != nil { for _, tag := range p.Tags { tagInput.Input(tag) p.SleepMs(300) tagInput.Input("\n") p.SleepMs(300) } p.LogInfo("标签设置成功") return nil } p.LogInfo("未找到标签输入框(可能无需设置)") return nil } func (p *SohuPublisher) clickPublish() error { p.LogInfo("点击发布按钮...") publishSelectors := []string{ ".publish-report-btn.active", "button:contains('发布')", ".publish-btn", ".submit-btn", "[class*='publish'] button", } var publishBtn *rod.Element var err error for _, selector := range publishSelectors { publishBtn, err = p.WaitForElementClickable(selector, 5) if err == nil && publishBtn != nil { visible, _ := publishBtn.Visible() if visible { p.LogInfo(fmt.Sprintf("找到发布按钮: %s", selector)) break } } } if publishBtn == nil { buttons, err := p.Page.Elements("button") if err == nil { for _, btn := range buttons { text, _ := btn.Text() if text == "发布" || strings.Contains(text, "发布") { publishBtn = btn p.LogInfo("通过遍历按钮找到发布按钮") break } } } } if publishBtn == nil { return fmt.Errorf("未找到发布按钮") } if _, err := publishBtn.Evaluate(&rod.EvalOptions{ JS: `el => el.scrollIntoView({block: 'center', behavior: 'smooth'})`, }); err != nil { p.LogInfo(fmt.Sprintf("滚动到按钮失败: %v", err)) } 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.Sleep(3) return nil } func (p *SohuPublisher) waitForPublishResult(timeout int) (bool, string) { p.LogInfo("等待发布结果...") startTime := time.Now() for time.Since(startTime) < time.Duration(timeout)*time.Second { currentURL := p.GetCurrentURL() if strings.Contains(currentURL, "contentManagement") { p.LogInfo(fmt.Sprintf("发布成功!URL: %s", currentURL)) return true, "发布成功" } successMsgs, _ := p.Page.Elements(".el-message--success, .toast-success, .message-success, [class*='success']") for _, elem := range successMsgs { visible, _ := elem.Visible() if visible { text, _ := elem.Text() if strings.Contains(text, "成功") || strings.Contains(strings.ToLower(text), "success") || strings.Contains(text, "已发布") { p.LogInfo(fmt.Sprintf("发布成功: %s", text)) return true, text } } } p.SleepMs(1000) } return false, "发布结果未知(超时)" } // InitPage 初始化页面 func (p *SohuPublisher) 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 *SohuPublisher) 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.inputTitle}, {"导入内容", p.inputContent}, } for _, step := range steps { if err := step.fn(); err != nil { p.LogStep(step.name, false, err.Error()) return false, fmt.Sprintf("%s失败: %s", step.name, err.Error()) } p.LogStep(step.name, true, "") } if err := p.clickPublish(); err != nil { p.LogStep("点击发布", false, err.Error()) return false, err.Error() } p.LogStep("点击发布", true, "") 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 }