geoGo/internal/publisher/baijiahao.go

549 lines
14 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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
}