524 lines
12 KiB
Go
524 lines
12 KiB
Go
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
|
||
}
|