549 lines
14 KiB
Go
549 lines
14 KiB
Go
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
|
||
}
|