package publisher import ( "fmt" "geo/internal/config" "geo/pkg" "log" "strings" "github.com/go-rod/rod" "github.com/go-rod/rod/lib/proto" ) type BaijiahaoPublisher struct { *BasePublisher Category string ArticleType string IsTop bool maxRetries int retryDelay int } func NewBaijiahaoPublisher(headless bool, title, content string, tags []string, tenantID, platIndex, requestID, imagePath, wordPath string, platInfo map[string]interface{}, cfg *config.Config, logger *log.Logger) PublisherInerface { base := NewBasePublisher(headless, title, content, tags, tenantID, platIndex, requestID, imagePath, wordPath, platInfo, cfg, logger) if platInfo != nil { base.LoginURL = pkg.GetString(platInfo, "login_url") base.EditorURL = pkg.GetString(platInfo, "edit_url") base.LoginedURL = pkg.GetString(platInfo, "logined_url") } return &BaijiahaoPublisher{ BasePublisher: base, maxRetries: 5, retryDelay: 2, } } func (p *BaijiahaoPublisher) CheckLoginStatus() bool { currentURL := p.GetCurrentURL() if strings.Contains(currentURL, p.LoginURL) { return false } return true } func (p *BaijiahaoPublisher) CheckLogin() (bool, string) { driverCreated := false defer func() { if driverCreated && p.Browser != nil { p.Close() } }() if err := p.SetupDriver(); err != nil { return false, fmt.Sprintf("浏览器启动失败: %v", err) } driverCreated = true p.Page.MustNavigate(p.EditorURL) p.Sleep(3) p.WaitForPageReady(5) if p.CheckLoginStatus() { p.SaveCookies() return true, "已登录" } return false, "未登录" } func (p *BaijiahaoPublisher) WaitLogin() (bool, string) { driverCreated := false defer func() { if driverCreated && p.Browser != nil { p.Close() } }() if err := p.SetupDriver(); err != nil { return false, fmt.Sprintf("浏览器启动失败: %v", err) } driverCreated = true 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.Sleep(1) if p.CheckLoginStatus() { p.SaveCookies() return true, "login_success" } } return false, "登录超时" } func (p *BaijiahaoPublisher) checkElementExists(selector string, timeout int) bool { _, err := p.WaitForElement(selector, timeout) return err == nil } func (p *BaijiahaoPublisher) PublishNote() (bool, string) { driverCreated := false defer func() { if driverCreated && p.Browser != nil { p.Close() } }() if err := p.SetupDriver(); err != nil { return false, fmt.Sprintf("浏览器启动失败: %v", err) } driverCreated = true p.Page.MustNavigate(p.EditorURL) p.Sleep(3) p.WaitForPageReady(5) if p.LoadCookies() == nil { p.RefreshPage() p.Sleep(3) if p.CheckLoginStatus() { return p.doPublish() } } if p.CheckLoginStatus() { p.SaveCookies() return p.doPublish() } return false, "需要登录" } func (p *BaijiahaoPublisher) doPublish() (bool, string) { p.LogInfo("开始发布百家号文章...") p.Sleep(3) steps := []struct { name string fn func() error }{ //{"切换到图文编辑模式", p.switchToGraphicMode}, {"输入内容", p.inputContent}, {"输入标题", p.inputTitle}, {"设置封面", p.uploadImage}, {"点击发布按钮", p.clickPublish}, {"处理确认弹窗", p.handleConfirmModal}, } 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() } func (p *BaijiahaoPublisher) switchToGraphicMode() error { p.LogInfo("切换到图文编辑模式...") tabSelectors := []string{ ".list-item.item-active", ".header-list-content .list-item", "div[role='tab']:first-child", } for _, selector := range tabSelectors { tab, err := p.Page.Element(selector) if err == nil && tab != nil { visible, _ := tab.Visible() if visible { p.JSClick(tab) p.LogInfo(fmt.Sprintf("已点击图文标签: %s", selector)) p.Sleep(1) return nil } } } return nil } func (p *BaijiahaoPublisher) inputTitle() error { p.LogInfo("输入文章标题...") titleSelectors := []string{ ".client_pages_edit_components_titleInput ._9ddb7e475b559749-editor", ".input-box ._9ddb7e475b559749-editor", "[contenteditable='true']", ".bjh-news-drag-tip + div [contenteditable='true']", } var titleInput *rod.Element for _, selector := range titleSelectors { titleInput, _ = p.WaitForElementVisible(selector, 5) if titleInput != nil { p.LogInfo(fmt.Sprintf("找到标题输入框: %s", selector)) break } } if titleInput == nil { return fmt.Errorf("未找到标题输入框") } titleInput.Click(proto.InputMouseButtonLeft, 1) p.SleepMs(500) currentTitle, _ := titleInput.Text() if currentTitle != "" { p.LogInfo(fmt.Sprintf("清空当前标题: %s", currentTitle[:min(50, len(currentTitle))])) p.ClearContentEditable(titleInput) p.SleepMs(300) titleInput.Input("\u0001") p.SleepMs(200) titleInput.Input("\u007F") p.SleepMs(200) } titleInput.Input(p.Title) p.LogInfo(fmt.Sprintf("新标题已输入: %s", p.Title)) p.triggerInputEvents(titleInput) p.SleepMs(500) finalTitle, _ := titleInput.Text() if finalTitle != p.Title { p.Page.Eval(fmt.Sprintf(`() => { arguments[0].innerHTML = '%s'; }`, p.Title)) p.triggerInputEvents(titleInput) p.LogInfo("已通过 JavaScript 重新设置标题") } return nil } func (p *BaijiahaoPublisher) inputContent() error { p.LogInfo("输入文章内容...") titleInput, err := p.Page.Element("[contenteditable='true']") if err != nil || titleInput == nil { return fmt.Errorf("未找到标题输入框") } p.LogInfo("从标题框按 Tab 键切换到内容编辑器") titleInput.Click(proto.InputMouseButtonLeft, 1) p.SleepMs(500) titleInput.Input("\t") p.LogInfo("已按 Tab 键") p.SleepMs(1500) contentEditor, err := p.Page.Element(".ProseMirror") if err != nil { contentEditor, err = p.Page.Element("[contenteditable='true']") } if contentEditor == nil { return fmt.Errorf("未找到内容编辑器") } contentEditor.Click(proto.InputMouseButtonLeft, 1) p.SleepMs(500) p.ClearContentEditable(contentEditor) p.SleepMs(300) p.SetContentEditable(contentEditor, p.Content) p.SleepMs(2000) inputContent, _ := contentEditor.Text() if len(inputContent) == 0 { contentEditor.Input(p.Content) p.SleepMs(2000) } return nil } func (p *BaijiahaoPublisher) uploadImage() error { if p.ImagePath == "" { p.LogInfo("未提供封面图片路径,跳过封面设置") return nil } p.LogInfo("设置文章封面...") p.SleepMs(2000) // 查找并点击封面选择区域 coverSelectors := []string{ ".cheetah-spin-container", "._73a3a52aab7e3a36-default", ".cover-selector", "[class*='spin-container']", } var coverArea *rod.Element for _, selector := range coverSelectors { coverArea, _ = p.WaitForElement(selector, 3) if coverArea != nil { visible, _ := coverArea.Visible() if visible { p.LogInfo(fmt.Sprintf("找到封面区域: %s", selector)) break } } } if coverArea != nil { p.ScrollToElement(coverArea) p.SleepMs(500) p.JSClick(coverArea) p.LogInfo("已点击封面选择区域") p.SleepMs(2000) } // 查找并点击上传区域 uploadSelectors := []string{ "div[class*='cheetah-upload']", ".cheetah-upload", "div[class*='upload']", ".upload-area", "._73a3a52aab7e3a36-content", "._93c3fe2a3121c388-item", } var uploadArea *rod.Element for _, selector := range uploadSelectors { elements, _ := p.Page.Elements(selector) for _, elem := range elements { visible, _ := elem.Visible() if visible { uploadArea = elem p.LogInfo(fmt.Sprintf("找到上传区域: %s", selector)) break } } if uploadArea != nil { break } } if uploadArea != nil { p.ScrollToElement(uploadArea) p.SleepMs(500) p.JSClick(uploadArea) p.LogInfo("已点击图片上传区域") p.SleepMs(1000) } // 查找cheetah-upload组件 componentSelectors := []string{ "div[class*='cheetah-upload']", ".cheetah-upload", "div[class*='upload']", } var uploadComponent *rod.Element for _, selector := range componentSelectors { elements, _ := p.Page.Elements(selector) for _, elem := range elements { visible, _ := elem.Visible() if visible { uploadComponent = elem p.LogInfo(fmt.Sprintf("找到cheetah-upload组件: %s", selector)) break } } if uploadComponent != nil { break } } if uploadComponent != nil { p.ScrollToElement(uploadComponent) p.SleepMs(500) p.JSClick(uploadComponent) p.LogInfo("已点击cheetah-upload上传组件") p.SleepMs(2000) } // 查找文件上传输入框 var fileInput *rod.Element for i := 0; i < 10; i++ { fileInput, _ = p.Page.Element("input[name='media'][type='file'][accept='image/*']") if fileInput != nil { p.LogInfo("找到文件上传输入框") break } fileInput, _ = p.Page.Element("input[type='file'][accept*='image']") if fileInput != nil { p.LogInfo("通过备用选择器找到文件上传输入框") break } p.SleepMs(500) } if fileInput != nil { fileInput.SetFiles([]string{p.ImagePath}) p.LogInfo(fmt.Sprintf("图片上传成功: %s", p.ImagePath)) p.Sleep(3) } // 查找并点击确认按钮 var confirmBtn *rod.Element for i := 0; i < 10; i++ { confirmBtn, _ = p.Page.ElementX("//button[contains(text(), '确定')]") if confirmBtn != nil { visible, _ := confirmBtn.Visible() if visible { p.LogInfo("通过文本找到确认按钮") break } } confirmBtn, _ = p.Page.Element(".cheetah-btn-primary") if confirmBtn != nil { text, _ := confirmBtn.Text() if strings.Contains(text, "确定") { p.LogInfo(fmt.Sprintf("通过CSS选择器找到确认按钮: %s", text)) break } } buttons, _ := p.Page.Elements("button[class*='cheetah-btn']") for _, btn := range buttons { visible, _ := btn.Visible() if visible { text, _ := btn.Text() if strings.Contains(text, "确定") || strings.Contains(text, "确认") { confirmBtn = btn p.LogInfo(fmt.Sprintf("通过遍历按钮找到确认按钮: %s", text)) break } } } if confirmBtn != nil { break } p.Sleep(1) } if confirmBtn != nil { p.ScrollToElement(confirmBtn) p.SleepMs(500) p.JSClick(confirmBtn) p.LogInfo("已点击确认按钮") p.SleepMs(2000) } return nil } func (p *BaijiahaoPublisher) clickPublish() error { p.LogInfo("点击发布按钮...") publishSelectors := []string{ "[data-testid='publish-btn']", ".op-list-right .cheetah-btn-primary", } var publishBtn *rod.Element for i := 0; i < 10; i++ { for _, selector := range publishSelectors { publishBtn, _ = p.Page.Element(selector) if publishBtn != nil { visible, _ := publishBtn.Visible() if visible { p.LogInfo(fmt.Sprintf("找到发布按钮: %s", selector)) break } } } if publishBtn != nil { break } publishBtn, _ = p.Page.ElementX("//button[contains(text(), '发布')]") if publishBtn != nil { visible, _ := publishBtn.Visible() if visible { p.LogInfo("通过XPath找到发布按钮") break } } p.Sleep(1) } if publishBtn == nil { return fmt.Errorf("未找到发布按钮") } p.ScrollToElement(publishBtn) p.Sleep(1) for attempt := 0; attempt < 3; attempt++ { err := p.JSClick(publishBtn) if err == nil { p.LogInfo(fmt.Sprintf("已通过JavaScript点击发布按钮 (尝试 %d)", attempt+1)) p.Sleep(3) return nil } err = publishBtn.Click(proto.InputMouseButtonLeft, 1) if err == nil { p.LogInfo(fmt.Sprintf("已通过普通点击发布按钮 (尝试 %d)", attempt+1)) p.Sleep(3) return nil } p.Sleep(1) } return fmt.Errorf("点击发布按钮失败") } func (p *BaijiahaoPublisher) handleConfirmModal() error { confirmBtn, _ := p.WaitForElement(".cheetah-modal .cheetah-btn-primary", 3) if confirmBtn != nil { p.JSClick(confirmBtn) p.LogInfo("已点击确认弹窗") p.Sleep(2) } return nil } func (p *BaijiahaoPublisher) waitForPublishResult() (bool, string) { p.LogInfo("等待发布结果...") for attempt := 0; attempt < 60; attempt++ { currentURL := p.GetCurrentURL() p.LogInfo(fmt.Sprintf("第 %d 次检查 - URL: %s", attempt+1, currentURL)) if strings.Contains(currentURL, "clue") { p.LogInfo(fmt.Sprintf("发布成功!URL: %s", currentURL)) return true, "发布成功" } elements, _ := p.Page.Elements(".cheetah-message-success, .cheetah-message-info") for _, elem := range elements { visible, _ := elem.Visible() if visible { text, _ := elem.Text() if strings.Contains(text, "成功") || strings.Contains(text, "发布") { p.LogInfo(fmt.Sprintf("发布成功: %s", text)) return true, text } } } elements, _ = p.Page.Elements(".cheetah-message-error, .cheetah-message-warning") for _, elem := range elements { visible, _ := elem.Visible() if visible { text, _ := elem.Text() if strings.Contains(text, "失败") || strings.Contains(text, "错误") { p.LogError(fmt.Sprintf("发布失败: %s", text)) return false, fmt.Sprintf("发布失败: %s", text) } } } p.Sleep(1) } return false, "发布结果未知" } func (p *BaijiahaoPublisher) triggerInputEvents(el *rod.Element) { el.Eval(`() => { arguments[0].dispatchEvent(new Event('input', {bubbles: true})); arguments[0].dispatchEvent(new Event('change', {bubbles: true})); arguments[0].dispatchEvent(new Event('blur', {bubbles: true})); }`) } func min(a, b int) int { if a < b { return a } return b }