geoGo/internal/publisher/wyh.go

623 lines
15 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 (
"context"
"fmt"
"geo/internal/config"
"log"
"os"
"strings"
"time"
"github.com/go-rod/rod"
"github.com/go-rod/rod/lib/proto"
)
type WangyiPublisher struct {
*BasePublisher
Category string
IsOriginal bool
}
// NewWangyiPublisher 构造函数
func NewWangyiPublisher(ctx context.Context, task *TaskParams, cfg *config.Config, logger *log.Logger) PublisherInerface {
return &WangyiPublisher{
BasePublisher: NewBasePublisher(ctx, task, cfg, logger),
Category: "",
IsOriginal: true,
}
}
func (p *WangyiPublisher) CheckLogin() (bool, string) {
p.LogInfo("检查登录状态...")
if err := p.SetupDriver(); err != nil {
return false, fmt.Sprintf("浏览器启动失败: %v", err)
}
defer p.Page.Close()
p.Page.MustNavigate(p.EditorURL)
p.Sleep(3)
p.WaitForPageReady(5)
if p.CheckLoginStatus() {
p.SaveCookies()
return true, "已登录"
}
return false, "未登录"
}
func (p *WangyiPublisher) CheckLoginStatus() bool {
currentURL := p.GetCurrentURL()
// 如果在登录页面,未登录
if strings.Contains(currentURL, p.LoginURL) {
return false
}
return true
}
func (p *WangyiPublisher) 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()
p.LogInfo("已有登录状态")
return true, "already_logged_in"
}
}
// 未登录,跳转到登录页
if p.LoginURL != "" {
p.Page.MustNavigate(p.LoginURL)
p.WaitForPageReady(5)
}
p.LogInfo("请扫码登录...")
// 等待登录完成最多120秒
for i := 0; i < 120; i++ {
currentURL := p.GetCurrentURL()
// 检查是否跳转到发布页面或主页
if p.EditorURL != "" && strings.Contains(currentURL, p.EditorURL) {
p.SaveCookies()
p.LogInfo("登录成功")
return true, "login_success"
}
if p.LoginedURL != "" && strings.Contains(currentURL, p.LoginedURL) {
p.SaveCookies()
p.LogInfo("登录成功")
return true, "login_success"
}
// 检查登录状态
if p.CheckLoginStatus() {
p.SaveCookies()
p.LogInfo("登录成功")
return true, "login_success"
}
time.Sleep(1 * time.Second)
}
return false, "登录超时,请检查网络或账号状态"
}
// inputTitle 输入标题
func (p *WangyiPublisher) inputTitle() error {
p.LogInfo("输入文章标题...")
titleSelectors := []string{
"textarea[placeholder*='标题']",
"input[placeholder*='标题']",
}
var titleInput *rod.Element
var err error
for _, selector := range titleSelectors {
titleInput, err = p.WaitForElementVisible(selector, 5)
if err == nil && titleInput != nil {
p.LogInfo(fmt.Sprintf("找到标题输入框: %s", selector))
break
}
}
if titleInput == nil {
return fmt.Errorf("未找到标题输入框")
}
// 点击获取焦点
if err := titleInput.Click(proto.InputMouseButtonLeft, 1); err != nil {
return fmt.Errorf("点击标题框失败: %v", err)
}
p.SleepMs(300)
// 清空输入框
if err := p.ClearInput(titleInput); err != nil {
titleInput.Input("")
}
p.SleepMs(300)
// 输入标题
if err := p.SetInputValue(titleInput, p.Title); err != nil {
titleInput.Input(p.Title)
}
p.LogInfo(fmt.Sprintf("标题已输入: %s", p.Title))
p.SleepMs(500)
return nil
}
// insertImageToWord 将图片插入到Word文档头部
func (p *WangyiPublisher) insertImageToWord(docPath, imagePath string) (string, error) {
// 由于Go中处理Word文档需要额外库这里先返回原路径
// 如果需要此功能,可以使用 unioffice 或 excelize 等库
p.LogInfo(fmt.Sprintf("插入图片到Word: %s -> %s", imagePath, docPath))
return docPath, nil
}
// importDocument 导入Word文档
func (p *WangyiPublisher) importDocument() error {
if p.SourcePath == "" {
p.LogInfo("未提供文档路径,使用内容文本")
return p.inputContentDirect()
}
if _, err := os.Stat(p.SourcePath); os.IsNotExist(err) {
p.LogWarning(fmt.Sprintf("文档不存在: %s", p.SourcePath))
return p.inputContentDirect()
}
p.LogInfo(fmt.Sprintf("尝试导入文档: %s", p.SourcePath))
// 查找导入文档按钮
importSelectors := []string{
".ne-rich-editor-upload-button",
"[class*='rich-editor-upload']",
"label[for='ne-rich-editor-upload-input']",
".rich-editor-panel-item",
}
var importBtn *rod.Element
var err error
for _, selector := range importSelectors {
importBtn, err = p.WaitForElementClickable(selector, 5)
if err == nil && importBtn != nil {
p.LogInfo(fmt.Sprintf("找到导入文档按钮: %s", selector))
break
}
}
if importBtn == nil {
// 尝试通过文本查找
importBtn, err = p.Page.Element("button:contains('导入文档')")
if err != nil {
p.LogWarning("未找到导入文档按钮,使用直接输入内容")
return p.inputContentDirect()
}
}
p.SleepMs(500)
// 查找文件上传输入框
fileInputSelectors := []string{
"#ne-rich-editor-upload-input",
"input[type='file'][accept*='.doc']",
"input[type='file'][accept*='.docx']",
".ne-rich-editor-upload-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.LogWarning("未找到文件上传输入框,使用直接输入内容")
return p.inputContentDirect()
}
// 上传文件
if err := fileInput.SetFiles([]string{p.SourcePath}); err != nil {
p.LogWarning(fmt.Sprintf("上传文件失败: %v使用直接输入内容", err))
return p.inputContentDirect()
}
p.LogInfo(fmt.Sprintf("文档已上传: %s", p.SourcePath))
p.SleepMs(3000)
// 等待导入完成
for attempt := 0; attempt < 30; attempt++ {
// 检查内容编辑器是否有内容
contentEditor, err := p.Page.Element(".rich-editor-stage, .ProseMirror, [contenteditable='true']")
if err == nil && contentEditor != nil {
text, _ := contentEditor.Text()
if len(text) > 0 {
p.LogInfo("文档导入成功")
return nil
}
}
p.SleepMs(1000)
}
p.LogInfo("文档导入完成")
return nil
}
// inputContentDirect 直接输入内容
func (p *WangyiPublisher) inputContentDirect() error {
p.LogInfo("直接输入文章内容...")
// 查找内容编辑器
contentSelectors := []string{
".rich-editor-stage",
".ProseMirror",
"[contenteditable='true']",
}
var contentEditor *rod.Element
var err error
for _, selector := range contentSelectors {
contentEditor, err = p.WaitForElementVisible(selector, 10)
if err == nil && contentEditor != nil {
p.LogInfo(fmt.Sprintf("找到内容编辑器: %s", selector))
break
}
}
if contentEditor == nil {
return fmt.Errorf("未找到内容编辑器")
}
// 点击获取焦点
if err := contentEditor.Click(proto.InputMouseButtonLeft, 1); err != nil {
return fmt.Errorf("点击编辑器失败: %v", err)
}
p.SleepMs(500)
// 清空现有内容
if err := p.ClearContentEditable(contentEditor); err != nil {
p.LogInfo(fmt.Sprintf("清空编辑器失败: %v", err))
}
p.SleepMs(300)
// 输入内容
if err := p.SetContentEditable(contentEditor, p.Content); err != nil {
contentEditor.Input(p.Content)
}
p.LogInfo(fmt.Sprintf("内容已输入,长度: %d", len(p.Content)))
return nil
}
// setCover 设置封面为自动模式
func (p *WangyiPublisher) setCover() error {
p.LogInfo("设置封面为自动模式...")
// 先找到 class="post-footer__container" 的 div点击
footerDiv, err := p.Page.ElementX("//span[contains(text(), '设置区')]")
if err != nil {
p.LogWarning("未找到 设置区")
return fmt.Errorf("未找到设置区")
}
// 点击这个div
if err := p.JSClick(footerDiv); err != nil {
p.LogInfo(fmt.Sprintf("点击 设置区 失败: %v", err))
}
p.SleepMs(2000)
// 查找包含"自动"文本的span
autoSpan, err := p.Page.ElementX("//span[contains(text(), '自动')]")
if err != nil {
p.LogWarning("未找到包含'自动'文本的span")
return fmt.Errorf("未找到包含'自动'文本的span")
}
p.SleepMs(500)
//
// 查找父级label并点击
parentLabel, err := autoSpan.Parent()
if err != nil {
return fmt.Errorf("未找到父级label元素: %v", err)
}
if err := p.JSClick(parentLabel); err != nil {
return fmt.Errorf("点击自动封面选项失败: %v", err)
}
p.LogInfo("已点击自动封面选项")
p.SleepMs(1000)
return nil
}
// clickPublish 点击发布按钮
func (p *WangyiPublisher) clickPublish() error {
p.LogInfo("点击发布按钮...")
publishSelectors := []string{
"button:contains('发布')",
"button.ne-button:contains('发布')",
"button.primary_button",
"button.ne-button-color-primary",
".netease-button.primary_button",
".publish-btn",
".submit-btn",
}
var publishBtn *rod.Element
var err error
for _, selector := range publishSelectors {
publishBtn, err = p.WaitForElementClickable(selector, 3)
if err == nil && publishBtn != nil {
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("未找到发布按钮")
}
p.SleepMs(500)
// 点击发布按钮
if err := p.JSClick(publishBtn); err != nil {
return fmt.Errorf("点击发布按钮失败: %v", err)
}
p.LogInfo("已点击发布按钮")
p.SleepMs(3000)
return nil
}
// waitForPublishResult 等待发布结果
func (p *WangyiPublisher) waitForPublishResult() (bool, string) {
p.LogInfo("等待发布结果...")
timeout := 60
retryInterval := 5
maxRetries := 5
lastClickTime := int64(0)
retryCount := 0
startTime := time.Now()
errorText := ""
for time.Since(startTime).Seconds() < float64(timeout) {
currentURL := p.GetCurrentURL()
// 检查是否发布成功
successKeywords := []string{"success", "article-manage", "content-manage", "list", "article/list"}
for _, keyword := range successKeywords {
if strings.Contains(currentURL, keyword) {
p.LogInfo(fmt.Sprintf("发布成功URL: %s", currentURL))
return true, "发布成功"
}
}
// 检查成功提示
successSelectors := []string{
"[class*='success']",
".toast-success",
".message-success",
".ne-message-success",
}
for _, selector := range successSelectors {
msgs, err := p.Page.Elements(selector)
if err == nil {
for _, elem := range msgs {
visible, _ := elem.Visible()
if visible {
text, _ := elem.Text()
if text != "" && (strings.Contains(text, "成功") || strings.Contains(text, "已发布")) {
p.LogInfo(fmt.Sprintf("发布成功: %s", text))
return true, text
}
}
}
}
}
// 检查错误提示
errorFound := false
errorSelectors := []string{
"[class*='error']",
".toast-error",
".message-error",
".ne-message-error",
}
for _, selector := range errorSelectors {
msgs, err := p.Page.Elements(selector)
if err == nil {
for _, elem := range msgs {
visible, _ := elem.Visible()
if visible {
text, _ := elem.Text()
if text != "" && (strings.Contains(text, "失败") || strings.Contains(text, "error")) {
errorText = text
errorFound = true
p.LogError(fmt.Sprintf("发布失败: %s", errorText))
break
}
}
}
}
if errorFound {
break
}
}
// 检查是否需要重试
currentTime := time.Now().Unix()
if currentTime-lastClickTime >= int64(retryInterval) && retryCount < maxRetries {
if strings.Contains(currentURL, p.EditorURL) {
p.LogInfo(fmt.Sprintf("第 %d 次重试,重新点击发布按钮...", retryCount+1))
// 如果有错误提示,先尝试关闭
if errorFound {
closeBtns, _ := p.Page.Elements(".close-btn, .ne-message-close, [class*='close']")
for _, btn := range closeBtns {
visible, _ := btn.Visible()
if visible {
p.JSClick(btn)
p.LogInfo("关闭错误提示")
p.SleepMs(1000)
break
}
}
}
// 重新点击发布按钮
if err := p.clickPublish(); err == nil {
retryCount++
lastClickTime = currentTime
errorFound = false
p.LogInfo(fmt.Sprintf("第 %d 次重试已点击发布按钮,继续等待结果...", retryCount))
} else {
p.LogWarning(fmt.Sprintf("第 %d 次重试点击发布按钮失败", retryCount+1))
lastClickTime = currentTime
retryCount++
}
p.SleepMs(2000)
continue
} else if errorFound {
return false, fmt.Sprintf("发布失败: %s", errorText)
}
}
p.SleepMs(1000)
}
// 超时后最后一次尝试
if retryCount < maxRetries {
p.LogInfo("超时前最后一次尝试点击发布按钮...")
p.clickPublish()
p.SleepMs(5000)
// 再次检查结果
successSelectors := []string{
"[class*='success']",
".toast-success",
".message-success",
}
for _, selector := range successSelectors {
msgs, err := p.Page.Elements(selector)
if err == nil {
for _, elem := range msgs {
text, _ := elem.Text()
if text != "" && strings.Contains(text, "成功") {
return true, text
}
}
}
}
}
return false, "发布结果未知(超时)"
}
// InitPage 初始化页面
func (p *WangyiPublisher) 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
}
// PublishNote 发布文章
func (p *WangyiPublisher) PublishNote() (bool, string) {
p.StartNote()
// 初始化浏览器
if err := p.SetupDriver(); err != nil {
return false, fmt.Sprintf("浏览器启动失败: %v", err)
}
defer p.Close()
// 执行发布流程
steps := []struct {
name string
fn func() error
}{
{"初始化页面", p.InitPage},
{"输入标题", p.inputTitle},
{"导入内容", p.importDocument},
{"设置封面", p.setCover},
{"点击发布", p.clickPublish},
}
for _, step := range steps {
if err := step.fn(); err != nil {
// 失败时截图
screenshotFile := fmt.Sprintf("wangyi_screenshot_%s_%d.png", step.name, time.Now().Unix())
p.Screenshot(screenshotFile)
p.LogStep(step.name, false, err.Error())
return false, fmt.Sprintf("%s失败: %v", step.name, err)
}
p.LogStep(step.name, true, "")
p.SleepMs(500)
}
// 等待发布结果
success, message := p.waitForPublishResult()
if success {
p.LogInfo(fmt.Sprintf("🎉 发布成功!%s", message))
} else {
p.LogError(fmt.Sprintf("发布失败: %s", message))
}
return success, message
}
// LogWarning 记录警告日志
func (p *WangyiPublisher) LogWarning(message string) {
p.Logger.Printf("⚠️ %s", message)
}