package publisher import ( "context" "fmt" "geo/internal/config" "github.com/go-rod/rod" "log" "strings" "time" ) // ShipinhaoVideoPublisher 视频号视频发布器 type ShipinhaoVideoPublisher struct { *BasePublisher iframe *rod.Page } // NewShipinhaoVideoPublisher 创建视频号发布器 func NewShipinhaoVideoPublisher(ctx context.Context, task *TaskParams, config *config.Config, logger *log.Logger) PublisherInerface { return &ShipinhaoVideoPublisher{ BasePublisher: NewBasePublisher(ctx, task, config, logger), } } func (p *ShipinhaoVideoPublisher) CheckLoginStatus() bool { currentURL := p.GetCurrentURL() if strings.Contains(currentURL, "login") || strings.Contains(currentURL, "passport") { return false } if strings.Contains(currentURL, "channels.weixin.qq.com") { return true } return false } func (p *ShipinhaoVideoPublisher) WaitLogin() (bool, string) { p.LogInfo("开始等待登录...") if err := p.SetupDriver(); err != nil { return false, fmt.Sprintf("浏览器启动失败: %v", err) } defer p.Close() p.Page.MustNavigate(p.LoginURL) p.Sleep(3) if p.CheckLoginStatus() { p.SaveCookies() return true, "already_logged_in" } p.LogInfo("请扫描二维码登录...") startTime := time.Now() timeout := 120 for time.Since(startTime) < time.Duration(timeout)*time.Second { if p.CheckLoginStatus() { p.SaveCookies() return true, "login_success" } p.SleepMs(2000) } return false, "登录超时" } // PublishNote 发布视频主流程 func (p *ShipinhaoVideoPublisher) PublishNote() (bool, string) { p.StartNote() // 1. 初始化浏览器 if err := p.SetupDriver(); err != nil { return false, fmt.Sprintf("浏览器启动失败: %v", err) } defer p.Browser.MustClose() // 3. 加载 cookies 并检查登录状态 if err := p.LoadCookies(); err == nil { p.Page.Navigate(p.EditorURL) p.Sleep(3) if !p.CheckLoginStatus() { return false, "需要登录" } p.LogInfo("登录状态正常") } p.SaveCookies() // 4. 确保在正确的 iframe 中 p.ensureInEditorIframe() p.Sleep(2) // 6. 按顺序尝试各种上传方法 uploadSuccess := false var uploadMessage string methods := []struct { name string fn func() error }{ {"CDP拦截上传", p.uploadViaCDPIntercept}, } for _, method := range methods { p.LogInfo(fmt.Sprintf("尝试 %s...", method.name)) err := method.fn() if err == nil { p.LogInfo(fmt.Sprintf("%s 成功", method.name)) uploadSuccess = true uploadMessage = fmt.Sprintf("%s成功", method.name) break } p.LogInfo(fmt.Sprintf("%s 失败: %v", method.name, err)) p.Sleep(1) } if !uploadSuccess { return false, fmt.Sprintf("所有上传方法均失败: %s", uploadMessage) } // 7. 等待上传完成 if success, msg := p.waitForUploadComplete(180); !success { p.LogInfo(fmt.Sprintf("上传等待可能未完成: %s", msg)) } // 8. 输入标题和描述 if success, msg := p.inputTitleAndDescription(); !success { return false, msg } // 9. 点击发布 if success, msg := p.clickPublish(); !success { return false, msg } // 10. 等待发布完成 currentURL := p.GetCurrentURL() if strings.Contains(currentURL, "https://channels.weixin.qq.com/platform/post/list") { p.LogInfo("发布完成") return true, "发布成功" } return false, "发布失败" } // ensureInEditorIframe 确保在编辑器 iframe 中 func (p *ShipinhaoVideoPublisher) ensureInEditorIframe() { p.LogInfo("切换到编辑器 iframe") // 先切换到默认内容 p.Page.MustElement("body") // 确保在根页面 iframeSelectors := []string{ "iframe[name='content']", "wujie-app iframe", "iframe[src*='content']", } for _, selector := range iframeSelectors { exist, frameElement, err := p.Page.Has(selector) if err == nil && exist { frame, err := frameElement.Frame() if err == nil && frame != nil { p.iframe = frame p.LogInfo(fmt.Sprintf("已切换到 iframe: %s", selector)) return } } } p.LogInfo("未找到 iframe,使用主页面") } // uploadViaCDPIntercept 使用 CDP 拦截并注入文件上传 func (p *ShipinhaoVideoPublisher) uploadViaCDPIntercept() error { p.LogInfo("使用 CDP 协议拦截文件上传...") p.Sleep(1) // 先在当前 iframe 中查找文件输入框 fileInputs, err := p.iframe.Elements("input[type='file'][accept*='video']") if err != nil { p.LogInfo(fmt.Sprintf("查找文件输入框失败: %v", err)) } if len(fileInputs) > 0 { p.LogInfo(fmt.Sprintf("找到 %d 个文件输入框,尝试直接设置文件", len(fileInputs))) err = fileInputs[0].SetFiles([]string{p.SourcePath}) if err == nil { p.LogInfo("直接设置文件成功") return nil } p.LogInfo(fmt.Sprintf("直接设置文件失败: %v", err)) } return nil } // waitForUploadComplete 等待上传完成 func (p *ShipinhaoVideoPublisher) waitForUploadComplete(timeout int) (bool, string) { p.LogInfo("等待视频上传完成...") startTime := time.Now() for time.Since(startTime).Seconds() < float64(timeout) { // 检查是否还存在上传区域特征 exists, _, err := p.iframe.Has(".form-item.flex-start") if err == nil && exists { p.LogInfo("视频上传成功") p.Sleep(2) return true, "上传完成" } p.Sleep(2) } return false, "上传超时" } // inputTitleAndDescription 输入标题和描述 func (p *ShipinhaoVideoPublisher) inputTitleAndDescription() (bool, string) { // 构建完整内容: "标题 #标签1 #标签2 #标签3" fullContent := p.Title if len(p.Tags) > 0 { var tagParts []string for _, tag := range p.Tags { if tag != "" { tagParts = append(tagParts, "#"+tag) } } if len(tagParts) > 0 { fullContent = fmt.Sprintf("%s %s", fullContent, strings.Join(tagParts, " ")) } } p.LogInfo(fmt.Sprintf("目标内容: %s", fullContent)) // 确保在正确的 iframe 中 exist, input, err := p.iframe.Has("div[data-placeholder*='描述']") if err != nil { return false, "未找到编辑器元素" } if !exist { return false, "未找到编辑器元素" } input.Input(fullContent) return true, "标签设置完成" } // clickPublish 点击发布按钮 //func (p *ShipinhaoVideoPublisher) clickPublish() (bool, string) { // p.LogInfo("点击发布按钮...") // p.Sleep(5) // // 直接用 JS 选择器,不经过 rod 元素 // _, err := p.iframe.Eval(`document.querySelector('div.form-btns button.weui-desktop-btn_primary')?.click()`) // if err != nil { // return false, fmt.Sprintf("点击按钮失败: %v", err) // } // // p.LogInfo("成功点击发表按钮") // return true, "" //} // clickPublish 点击发布按钮 func (p *ShipinhaoVideoPublisher) clickPublish() (bool, string) { p.LogInfo("点击发布按钮...") p.Sleep(5) // 直接在 iframe 上执行 JS,通过选择器找到并点击 p.iframe.Eval(`document.querySelector('div.form-btns button.weui-desktop-btn_primary')?.click()`) p.LogInfo("成功点击发表按钮") return true, "" }