package publisher import ( "fmt" "strings" "time" "geo/internal/config" "github.com/go-rod/rod" "github.com/go-rod/rod/lib/proto" ) type BaijiahaoPublisher struct { *BasePublisher Category string ArticleType string IsTop bool } func NewBaijiahaoPublisher(headless bool, title, content string, tags []string, tenantID, platIndex, requestID, imagePath, wordPath string, platInfo map[string]interface{}, cfg *config.Config) *BaijiahaoPublisher { base := NewBasePublisher(headless, title, content, tags, tenantID, platIndex, requestID, imagePath, wordPath, platInfo, cfg) if platInfo != nil { base.LoginURL = getString(platInfo, "login_url") base.EditorURL = getString(platInfo, "edit_url") base.LoginedURL = getString(platInfo, "logined_url") } return &BaijiahaoPublisher{BasePublisher: base} } func (p *BaijiahaoPublisher) CheckLoginStatus() bool { url := p.GetCurrentURL() // 如果URL包含登录相关关键词,表示未登录 if strings.Contains(url, "login") || strings.Contains(url, "passport") { return false } // 如果URL是编辑页面或主页,表示已登录 if strings.Contains(url, "baijiahao") || strings.Contains(url, "edit") { return true } return url != p.LoginURL } func (p *BaijiahaoPublisher) CheckLogin() (bool, string) { p.LogInfo("检查登录状态...") if err := p.SetupDriver(); err != nil { return false, fmt.Sprintf("浏览器启动失败: %v", err) } defer p.Close() p.Page.MustNavigate(p.LoginedURL) p.Sleep(3) p.WaitForPageReady(5) if p.CheckLoginStatus() { p.SaveCookies() return true, "已登录" } return false, "未登录" } func (p *BaijiahaoPublisher) WaitLogin() (bool, string) { p.LogInfo("开始等待登录...") if err := p.SetupDriver(); err != nil { return false, fmt.Sprintf("浏览器启动失败: %v", err) } defer p.Close() // 先尝试访问已登录页面 p.Page.MustNavigate(p.LoginedURL) p.Sleep(3) if p.CheckLoginStatus() { p.SaveCookies() p.LogInfo("已有登录状态") return true, "already_logged_in" } // 未登录,跳转到登录页 p.Page.MustNavigate(p.LoginURL) p.LogInfo("请扫描二维码登录...") // 等待登录完成,最多120秒 for i := 0; i < 120; i++ { time.Sleep(1 * time.Second) if p.CheckLoginStatus() { p.SaveCookies() p.LogInfo("登录成功") return true, "login_success" } } return false, "登录超时" } func (p *BaijiahaoPublisher) inputTitle() error { p.LogInfo("输入标题...") titleSelectors := []string{ ".client_pages_edit_components_titleInput [contenteditable='true']", ".input-box [contenteditable='true']", "[contenteditable='true']", } 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(500) // 清空输入框 if err := p.ClearContentEditable(titleInput); err != nil { p.LogInfo(fmt.Sprintf("清空标题框失败: %v", err)) } p.SleepMs(300) // 输入标题 if err := p.SetContentEditable(titleInput, p.Title); err != nil { // 备用输入方式 titleInput.Input(p.Title) } p.LogInfo(fmt.Sprintf("标题已输入: %s", p.Title)) return nil } func (p *BaijiahaoPublisher) inputContent() error { p.LogInfo("输入内容...") // 查找内容编辑器 contentEditor, err := p.WaitForElementVisible(".ProseMirror", 10) if err != nil { contentEditor, err = p.WaitForElementVisible("[contenteditable='true']", 10) if err != nil { return fmt.Errorf("未找到内容编辑器: %v", err) } } // 点击获取焦点 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 } func (p *BaijiahaoPublisher) uploadImage() error { if p.ImagePath == "" { p.LogInfo("无封面图片,跳过") return nil } p.LogInfo(fmt.Sprintf("上传封面: %s", p.ImagePath)) // 查找封面区域 coverArea, err := p.WaitForElementClickable(".cheetah-spin-container", 5) if err != nil { p.LogInfo("未找到封面区域,跳过") return nil } if err := coverArea.Click(proto.InputMouseButtonLeft, 1); err != nil { p.LogInfo(fmt.Sprintf("点击封面区域失败: %v", err)) } p.SleepMs(1000) // 查找文件输入框 fileInput, err := p.Page.Element("input[type='file'][accept*='image']") if err != nil { fileInput, err = p.Page.Element("input[type='file']") if err != nil { return fmt.Errorf("未找到文件输入框: %v", err) } } // 上传图片 if err := fileInput.SetFiles([]string{p.ImagePath}); err != nil { return fmt.Errorf("上传图片失败: %v", err) } p.LogInfo("图片上传成功") p.Sleep(3) // 查找确认按钮 confirmBtn, err := p.WaitForElementClickable(".cheetah-btn-primary", 5) if err == nil && confirmBtn != nil { if err := confirmBtn.Click(proto.InputMouseButtonLeft, 1); err != nil { p.LogInfo(fmt.Sprintf("点击确认按钮失败: %v", err)) } p.LogInfo("已确认封面") p.SleepMs(1000) } return nil } func (p *BaijiahaoPublisher) clickPublish() error { p.LogInfo("点击发布按钮...") // 滚动到底部 if _, err := p.Page.Eval(`() => window.scrollTo(0, document.body.scrollHeight)`); err != nil { p.LogInfo(fmt.Sprintf("滚动到底部失败: %v", err)) } p.SleepMs(1000) // 查找发布按钮 publishSelectors := []string{ "[data-testid='publish-btn']", ".op-list-right .cheetah-btn-primary", ".cheetah-btn-primary", "button:contains('发布')", } var publishBtn *rod.Element var err error for _, selector := range publishSelectors { publishBtn, err = p.WaitForElementClickable(selector, 5) if err == nil && publishBtn != nil { p.LogInfo(fmt.Sprintf("找到发布按钮: %s", selector)) break } } // 如果还是没找到,通过 XPath 查找 if publishBtn == nil { publishBtn, err = p.Page.ElementX("//button[contains(text(), '发布')]") if err != nil { return fmt.Errorf("未找到发布按钮: %v", err) } } // 滚动到按钮位置 if err := p.ScrollToElement(publishBtn); err != nil { p.LogInfo(fmt.Sprintf("滚动到按钮失败: %v", err)) } p.SleepMs(500) // 点击发布 if err := publishBtn.Click(proto.InputMouseButtonLeft, 1); err != nil { return fmt.Errorf("点击发布按钮失败: %v", err) } p.LogInfo("已点击发布按钮") return nil } func (p *BaijiahaoPublisher) waitForPublishResult() (bool, string) { p.LogInfo("等待发布结果...") // 等待最多60秒 for i := 0; i < 60; i++ { p.SleepMs(1000) // 检查URL是否跳转到成功页面 currentURL := p.GetCurrentURL() if strings.Contains(currentURL, "clue") || strings.Contains(currentURL, "success") || strings.Contains(currentURL, "article/list") { p.LogInfo("发布成功!") return true, "发布成功" } // 检查是否有成功提示 elements, _ := p.Page.Elements(".cheetah-message-success, .cheetah-message-info, [class*='success']") for _, el := range elements { text, _ := el.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, [class*='error']") for _, el := range elements { text, _ := el.Text() if strings.Contains(text, "失败") || strings.Contains(text, "错误") { p.LogError(fmt.Sprintf("发布失败: %s", text)) return false, text } } } return false, "发布结果未知(超时)" } func (p *BaijiahaoPublisher) PublishNote() (bool, string) { p.LogInfo(strings.Repeat("=", 50)) p.LogInfo("开始发布百家号文章...") p.LogInfo(fmt.Sprintf("标题: %s", p.Title)) p.LogInfo(fmt.Sprintf("内容长度: %d", len(p.Content))) p.LogInfo(strings.Repeat("=", 50)) // 初始化浏览器 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) // 尝试加载cookies if err := p.LoadCookies(); err == nil { p.RefreshPage() p.Sleep(2) if p.CheckLoginStatus() { p.LogInfo("使用cookies登录成功") } else { p.LogInfo("cookies已过期,需要重新登录") return false, "需要登录" } } // 检查登录状态 if !p.CheckLoginStatus() { return false, "需要登录" } // 保存cookies p.SaveCookies() // 执行发布流程 steps := []struct { name string fn func() error }{ {"输入标题", p.inputTitle}, {"输入内容", p.inputContent}, {"上传封面", p.uploadImage}, } 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) } // 点击发布 if err := p.clickPublish(); err != nil { return false, err.Error() } // 等待发布结果 return p.waitForPublishResult() }