geoGo/internal/publisher/sphsp.go

276 lines
6.8 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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, ""
}