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) 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) }