405 lines
9.6 KiB
Go
405 lines
9.6 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 ZhihuPublisher struct {
|
||
*BasePublisher
|
||
}
|
||
|
||
func NewZhihuPublisher(ctx context.Context, task *TaskParams, cfg *config.Config, logger *log.Logger) PublisherInerface {
|
||
return &ZhihuPublisher{NewBasePublisher(ctx, task, cfg, logger)}
|
||
}
|
||
|
||
func (p *ZhihuPublisher) 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 *ZhihuPublisher) CheckLoginStatus() bool {
|
||
currentURL := p.GetCurrentURL()
|
||
if strings.Contains(currentURL, p.LoginURL) {
|
||
return false
|
||
}
|
||
return true
|
||
}
|
||
|
||
func (p *ZhihuPublisher) WaitLogin() (bool, string) {
|
||
p.LogInfo("开始等待登录...")
|
||
|
||
if err := p.SetupDriver(); err != nil {
|
||
return false, fmt.Sprintf("浏览器启动失败: %v", err)
|
||
}
|
||
defer p.Close()
|
||
|
||
if p.EditorURL != "" {
|
||
p.Page.MustNavigate(p.EditorURL)
|
||
p.Sleep(3)
|
||
|
||
if p.CheckLoginStatus() {
|
||
p.SaveCookies()
|
||
return true, "already_logged_in"
|
||
}
|
||
}
|
||
|
||
startTime := time.Now()
|
||
timeout := 240
|
||
for time.Since(startTime) < time.Duration(timeout)*time.Second {
|
||
currentURL := p.GetCurrentURL()
|
||
if p.EditorURL != "" && strings.Contains(currentURL, p.EditorURL) {
|
||
p.SaveCookies()
|
||
return true, "login_success"
|
||
}
|
||
if p.LoginedURL != "" && strings.Contains(currentURL, p.LoginedURL) {
|
||
p.SaveCookies()
|
||
return true, "login_success"
|
||
}
|
||
if p.CheckLoginStatus() {
|
||
p.SaveCookies()
|
||
return true, "login_success"
|
||
}
|
||
p.SleepMs(1000)
|
||
}
|
||
|
||
return false, "登录超时,请检查网络或账号状态"
|
||
}
|
||
|
||
func (p *ZhihuPublisher) waitForEditorReady(timeout int) bool {
|
||
p.LogInfo("等待编辑器加载...")
|
||
startTime := time.Now()
|
||
for time.Since(startTime) < time.Duration(timeout)*time.Second {
|
||
titleSelectors := []string{
|
||
".WriteIndex-titleInput textarea",
|
||
".DraftEditor-root",
|
||
".public-DraftEditor-content",
|
||
"[contenteditable='true']",
|
||
}
|
||
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 *ZhihuPublisher) inputTitle() error {
|
||
p.LogInfo("输入文章标题...")
|
||
|
||
titleSelectors := []string{
|
||
".WriteIndex-titleInput textarea",
|
||
"textarea[placeholder*='标题']",
|
||
".Input[placeholder*='标题']",
|
||
".title-input textarea",
|
||
}
|
||
|
||
for _, selector := range titleSelectors {
|
||
titleInput, err := p.WaitForElementVisible(selector, 5)
|
||
if err == nil && titleInput != nil {
|
||
p.LogInfo(fmt.Sprintf("找到标题输入框: %s", selector))
|
||
|
||
p.ClearInput(titleInput)
|
||
p.SleepMs(300)
|
||
titleInput.Input("")
|
||
p.SleepMs(300)
|
||
titleInput.Input(p.Title)
|
||
|
||
p.LogInfo(fmt.Sprintf("标题已输入: %s", p.Title))
|
||
|
||
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 *ZhihuPublisher) importDocument() error {
|
||
if p.SourcePath == "" {
|
||
p.LogInfo("未提供文档路径或文档不存在")
|
||
return fmt.Errorf("文档不存在")
|
||
}
|
||
|
||
p.LogInfo(fmt.Sprintf("尝试导入文档: %s", p.SourcePath))
|
||
|
||
// 步骤1: 查找并点击"导入"按钮
|
||
p.LogInfo("步骤1: 查找并点击'导入'按钮...")
|
||
|
||
importBtn, err := p.Page.Element("button[aria-label='导入']")
|
||
if err != nil {
|
||
buttons, _ := p.Page.Elements("button")
|
||
for _, btn := range buttons {
|
||
text, _ := btn.Text()
|
||
if text == "导入" {
|
||
importBtn = btn
|
||
p.LogInfo("通过文本找到导入按钮")
|
||
break
|
||
}
|
||
}
|
||
}
|
||
|
||
if importBtn == nil {
|
||
return fmt.Errorf("未找到导入按钮")
|
||
}
|
||
|
||
importBtn.Click(proto.InputMouseButtonLeft, 1)
|
||
p.LogInfo("已点击导入按钮")
|
||
p.SleepMs(1000)
|
||
|
||
// 步骤2: 查找并点击"导入文档"按钮
|
||
p.LogInfo("步骤2: 查找并点击'导入文档'按钮...")
|
||
|
||
docImportBtn, err := p.Page.Element("button[aria-label='导入文档']")
|
||
if err != nil {
|
||
menuButtons, _ := p.Page.Elements(".Menu button, .Popover-content button")
|
||
for _, btn := range menuButtons {
|
||
text, _ := btn.Text()
|
||
if strings.Contains(text, "导入文档") {
|
||
docImportBtn = btn
|
||
p.LogInfo("通过文本找到导入文档按钮")
|
||
break
|
||
}
|
||
}
|
||
}
|
||
|
||
if docImportBtn == nil {
|
||
return fmt.Errorf("未找到导入文档按钮")
|
||
}
|
||
|
||
docImportBtn.Click(proto.InputMouseButtonLeft, 1)
|
||
p.LogInfo("已点击导入文档按钮")
|
||
p.SleepMs(1000)
|
||
|
||
// 步骤3: 查找file input并上传文档
|
||
p.LogInfo("步骤3: 查找文件上传输入框...")
|
||
|
||
var fileInput *rod.Element
|
||
for i := 0; i < 10; i++ {
|
||
fileInput, _ = p.Page.Element("input[type='file'][accept='.docx,.markdown,.mdown,.mkdn,.md']")
|
||
if fileInput != nil {
|
||
p.LogInfo("找到导入文档输入框")
|
||
break
|
||
}
|
||
if fileInput == nil {
|
||
hiddenInputs, _ := p.Page.Elements("input[type='file'][style*='display: none']")
|
||
for _, inp := range hiddenInputs {
|
||
accept, _ := inp.Attribute("accept")
|
||
if accept != nil && (strings.Contains(*accept, ".docx") || strings.Contains(*accept, ".md")) {
|
||
fileInput = inp
|
||
p.LogInfo("找到隐藏的导入文档输入框")
|
||
break
|
||
}
|
||
}
|
||
}
|
||
if fileInput != nil {
|
||
break
|
||
}
|
||
p.SleepMs(500)
|
||
}
|
||
|
||
if fileInput == nil {
|
||
return fmt.Errorf("未找到文件上传输入框")
|
||
}
|
||
|
||
p.LogInfo(fmt.Sprintf("开始上传文档: %s", p.SourcePath))
|
||
fileInput.SetFiles([]string{p.SourcePath})
|
||
p.LogInfo(fmt.Sprintf("文档已上传: %s", p.SourcePath))
|
||
p.Sleep(3)
|
||
|
||
// 等待导入完成
|
||
success := false
|
||
for i := 0; i < 60; i++ {
|
||
p.SleepMs(1000)
|
||
|
||
toasts, _ := p.Page.Elements(".Toast-module_toast, .el-message--success, .toast-success")
|
||
for _, toast := range toasts {
|
||
visible, _ := toast.Visible()
|
||
if visible {
|
||
text, _ := toast.Text()
|
||
if strings.Contains(text, "成功") || strings.Contains(text, "导入") || strings.Contains(text, "完成") {
|
||
p.LogInfo(fmt.Sprintf("导入成功提示: %s", text))
|
||
success = true
|
||
break
|
||
}
|
||
}
|
||
}
|
||
if success {
|
||
break
|
||
}
|
||
|
||
editors, _ := p.Page.Elements("[contenteditable='true'], .DraftEditor-root, .ProseMirror")
|
||
for _, editor := range editors {
|
||
text, _ := editor.Text()
|
||
if len(text) > 10 {
|
||
p.LogInfo(fmt.Sprintf("文档导入成功,内容长度: %d", len(text)))
|
||
success = true
|
||
break
|
||
}
|
||
}
|
||
if success {
|
||
break
|
||
}
|
||
}
|
||
|
||
if success {
|
||
p.SleepMs(2000)
|
||
p.LogInfo("文档导入完成")
|
||
return nil
|
||
}
|
||
|
||
p.LogInfo("文档导入状态未知")
|
||
return nil
|
||
}
|
||
|
||
func (p *ZhihuPublisher) clickPublish() error {
|
||
p.LogInfo("点击发布按钮...")
|
||
|
||
publishSelectors := []string{
|
||
".Button--primary:contains('发布')",
|
||
".css-d0uhtl",
|
||
"button:contains('发布')",
|
||
".PublishButton",
|
||
"[class*='publish'] button.Button--primary",
|
||
}
|
||
|
||
var publishBtn *rod.Element
|
||
for _, selector := range publishSelectors {
|
||
publishBtn, _ = p.WaitForElementClickable(selector, 5)
|
||
if publishBtn != nil {
|
||
visible, _ := publishBtn.Visible()
|
||
if visible {
|
||
p.LogInfo(fmt.Sprintf("找到发布按钮: %s", selector))
|
||
break
|
||
}
|
||
}
|
||
}
|
||
|
||
if publishBtn == nil {
|
||
buttons, _ := p.Page.Elements("button")
|
||
for _, btn := range buttons {
|
||
visible, _ := btn.Visible()
|
||
if visible {
|
||
text, _ := btn.Text()
|
||
if text == "发布" || strings.Contains(text, "发布") {
|
||
publishBtn = btn
|
||
p.LogInfo("通过遍历按钮找到发布按钮")
|
||
break
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
if publishBtn == nil {
|
||
return fmt.Errorf("未找到发布按钮")
|
||
}
|
||
|
||
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 *ZhihuPublisher) 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, "/edit") {
|
||
p.LogInfo(fmt.Sprintf("发布成功!URL: %s", currentURL))
|
||
return true, "发布成功"
|
||
}
|
||
// 检查失败弹窗
|
||
exist, failedDiv, _ := p.Page.HasX(".Notification-textSection")
|
||
if exist {
|
||
failedReason, _ := failedDiv.Text()
|
||
p.LogInfo(fmt.Sprintf("发布失败: %s", failedReason))
|
||
return false, failedReason
|
||
}
|
||
|
||
p.SleepMs(1000)
|
||
}
|
||
|
||
return false, "发布结果未知(超时)"
|
||
}
|
||
|
||
func (p *ZhihuPublisher) 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.clickPublish},
|
||
}
|
||
|
||
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, "")
|
||
}
|
||
|
||
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
|
||
}
|