276 lines
6.8 KiB
Go
276 lines
6.8 KiB
Go
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, ""
|
||
}
|