geoGo/internal/publisher/sphsp.go

583 lines
15 KiB
Go

package publisher
import (
"encoding/base64"
"fmt"
"geo/internal/config"
"log"
"os"
"path/filepath"
"strings"
"time"
)
type ShipinhaoVideoPublisher struct {
*BasePublisher
shortWait int
mediumWait int
}
func NewShipinhaoVideoPublisher(task *TaskParams, cfg *config.Config, logger *log.Logger) PublisherInerface {
return &ShipinhaoVideoPublisher{
BasePublisher: NewBasePublisher(task, cfg, logger),
shortWait: 1,
mediumWait: 3,
}
}
func (p *ShipinhaoVideoPublisher) 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 *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, "登录超时"
}
func (p *ShipinhaoVideoPublisher) ensureInEditorIframe() error {
p.Page.Timeout(5 * time.Second).Eval(`() => {
const iframes = document.querySelectorAll('iframe[name="content"], wujie-app iframe, iframe[src*="content"]');
if (iframes.length > 0) {
return true;
}
return false;
}`)
return nil
}
func (p *ShipinhaoVideoPublisher) uploadViaCdpIntercept(filePath string) (bool, string) {
p.LogInfo("使用 CDP 协议拦截文件上传...")
fileData, err := os.ReadFile(filePath)
if err != nil {
return false, fmt.Sprintf("读取文件失败: %v", err)
}
fileDataBase64 := base64.StdEncoding.EncodeToString(fileData)
fileName := filepath.Base(filePath)
p.ensureInEditorIframe()
p.SleepMs(1000)
script := fmt.Sprintf(`
(function() {
var byteCharacters = atob('%s');
var byteNumbers = new Array(byteCharacters.length);
for (var i = 0; i < byteCharacters.length; i++) {
byteNumbers[i] = byteCharacters.charCodeAt(i);
}
var byteArray = new Uint8Array(byteNumbers);
var blob = new Blob([byteArray], {type: 'video/mp4'});
var file = new File([blob], '%s', {type: 'video/mp4'});
var fileInput = document.querySelector('input[type="file"]');
if (!fileInput) {
fileInput = document.createElement('input');
fileInput.type = 'file';
fileInput.accept = 'video/*';
fileInput.style.position = 'fixed';
fileInput.style.top = '-1000px';
fileInput.style.left = '-1000px';
document.body.appendChild(fileInput);
}
var dataTransfer = new DataTransfer();
dataTransfer.items.add(file);
fileInput.files = dataTransfer.files;
var changeEvent = new Event('change', { bubbles: true });
fileInput.dispatchEvent(changeEvent);
var inputEvent = new Event('input', { bubbles: true });
fileInput.dispatchEvent(inputEvent);
var uploadAreas = document.querySelectorAll('[class*="upload"], [class*="drop"]');
for (var i = 0; i < uploadAreas.length; i++) {
var area = uploadAreas[i];
if (area.offsetParent !== null) {
var dragOverEvent = new DragEvent('dragover', {
bubbles: true,
cancelable: true,
dataTransfer: dataTransfer
});
area.dispatchEvent(dragOverEvent);
var dropEvent = new DragEvent('drop', {
bubbles: true,
cancelable: true,
dataTransfer: dataTransfer
});
area.dispatchEvent(dropEvent);
break;
}
}
return {success: true, fileName: '%s'};
})();
`, fileDataBase64, fileName, fileName)
result, err := p.Page.Eval(script)
if err != nil {
return false, err.Error()
}
p.LogInfo(fmt.Sprintf("CDP 注入完成: %v", result))
return true, "文件已注入"
}
func (p *ShipinhaoVideoPublisher) uploadViaDragEvent(filePath string) (bool, string) {
p.LogInfo("模拟拖拽事件上传...")
fileData, err := os.ReadFile(filePath)
if err != nil {
return false, fmt.Sprintf("读取文件失败: %v", err)
}
fileDataBase64 := base64.StdEncoding.EncodeToString(fileData)
fileName := filepath.Base(filePath)
p.ensureInEditorIframe()
p.SleepMs(1000)
script := fmt.Sprintf(`
(function() {
var byteCharacters = atob('%s');
var byteNumbers = new Array(byteCharacters.length);
for (var i = 0; i < byteCharacters.length; i++) {
byteNumbers[i] = byteCharacters.charCodeAt(i);
}
var byteArray = new Uint8Array(byteNumbers);
var blob = new Blob([byteArray], {type: 'video/mp4'});
var file = new File([blob], '%s', {type: 'video/mp4'});
var dataTransfer = new DataTransfer();
dataTransfer.items.add(file);
var dropZones = document.querySelectorAll('[class*="upload"], [class*="drop"], [class*="video"]');
var targetZone = null;
for (var i = 0; i < dropZones.length; i++) {
var zone = dropZones[i];
if (zone.offsetParent !== null &&
(zone.innerText.includes('上传') ||
zone.innerText.includes('时长') ||
zone.className.includes('upload'))) {
targetZone = zone;
break;
}
}
if (!targetZone) {
targetZone = document.body;
}
var dragOverEvent = new DragEvent('dragover', {
bubbles: true,
cancelable: true,
dataTransfer: dataTransfer
});
targetZone.dispatchEvent(dragOverEvent);
var dropEvent = new DragEvent('drop', {
bubbles: true,
cancelable: true,
dataTransfer: dataTransfer
});
targetZone.dispatchEvent(dropEvent);
return {success: true, message: '拖拽事件已触发'};
})();
`, fileDataBase64, fileName)
result, err := p.Page.Eval(script)
if err != nil {
return false, err.Error()
}
p.LogInfo(fmt.Sprintf("拖拽事件已触发: %v", result))
return true, "拖拽事件已触发"
}
func (p *ShipinhaoVideoPublisher) uploadViaReactEvent(filePath string) (bool, string) {
p.LogInfo("尝试 React 事件上传...")
fileData, err := os.ReadFile(filePath)
if err != nil {
return false, fmt.Sprintf("读取文件失败: %v", err)
}
fileDataBase64 := base64.StdEncoding.EncodeToString(fileData)
fileName := filepath.Base(filePath)
p.ensureInEditorIframe()
script := fmt.Sprintf(`
(function() {
var allElements = document.querySelectorAll('*');
var uploadComponent = null;
for (var i = 0; i < allElements.length; i++) {
var el = allElements[i];
if (el._reactRootContainer ||
Object.keys(el).some(key => key.startsWith('__react'))) {
if (el.innerText &&
(el.innerText.includes('上传') ||
el.innerText.includes('时长'))) {
uploadComponent = el;
break;
}
}
}
if (uploadComponent) {
var byteCharacters = atob('%s');
var byteNumbers = new Array(byteCharacters.length);
for (var i = 0; i < byteCharacters.length; i++) {
byteNumbers[i] = byteCharacters.charCodeAt(i);
}
var byteArray = new Uint8Array(byteNumbers);
var blob = new Blob([byteArray], {type: 'video/mp4'});
var file = new File([blob], '%s', {type: 'video/mp4'});
var dataTransfer = new DataTransfer();
dataTransfer.items.add(file);
var fileInput = document.querySelector('input[type="file"]');
if (fileInput) {
fileInput.files = dataTransfer.files;
var event = new Event('change', {bubbles: true});
fileInput.dispatchEvent(event);
}
var syntheticEvent = new Event('change', {bubbles: true});
syntheticEvent.target = {files: dataTransfer.files};
uploadComponent.dispatchEvent(syntheticEvent);
return {success: true};
}
return {success: false, message: '未找到 React 组件'};
})();
`, fileDataBase64, fileName)
result, err := p.Page.Eval(script)
if err != nil {
return false, err.Error()
}
p.LogInfo(fmt.Sprintf("React 事件触发结果: %v", result))
return true, "React事件上传成功"
}
func (p *ShipinhaoVideoPublisher) waitForUploadComplete(timeout int) (bool, string) {
p.LogInfo("等待视频上传完成...")
startTime := time.Now()
for time.Since(startTime) < time.Duration(timeout)*time.Second {
uploadAreas, err := p.Page.Elements(".form-item.flex-start")
if err == nil && len(uploadAreas) > 0 {
p.LogInfo("视频上传成功")
p.SleepMs(2000)
return true, "上传完成"
}
p.SleepMs(2000)
}
return false, "上传超时"
}
func (p *ShipinhaoVideoPublisher) inputTitleAndDescription() (bool, string) {
fullContent := p.Title
if len(p.Tags) > 0 {
tagStr := ""
for _, tag := range p.Tags {
if tag != "" {
tagStr += fmt.Sprintf("#%s ", strings.TrimSpace(tag))
}
}
tagStr = strings.TrimSpace(tagStr)
if tagStr != "" {
fullContent = fmt.Sprintf("%s %s", fullContent, tagStr)
}
}
p.LogInfo(fmt.Sprintf("目标内容: %s", fullContent))
p.ensureInEditorIframe()
p.SleepMs(1000)
jsScript := fmt.Sprintf(`
function setEditorContent(content) {
var editor = document.querySelector('.post-desc-box .input-editor, .input-editor, [contenteditable="true"]');
if (!editor) {
console.error('Editor element not found');
return false;
}
editor.focus();
editor.innerText = '';
editor.innerText = content;
var events = ['input', 'change', 'blur', 'focus', 'keyup', 'keydown'];
events.forEach(function(eventType) {
var event = new Event(eventType, { bubbles: true, cancelable: true });
editor.dispatchEvent(event);
});
var nativeInputValueSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value')?.set;
if (nativeInputValueSetter && editor.tagName === 'INPUT') {
nativeInputValueSetter.call(editor, content);
editor.dispatchEvent(new Event('input', { bubbles: true }));
} else {
var reactKey = Object.keys(editor).find(function(key) { return key.startsWith('__reactEventHandlers'); });
if (reactKey && editor[reactKey] && editor[reactKey].onChange) {
var syntheticEvent = { target: { value: content, innerText: content }, type: 'change' };
editor[reactKey].onChange(syntheticEvent);
}
}
var customEvent = new CustomEvent('react-change', {
bubbles: true,
detail: { value: content }
});
editor.dispatchEvent(customEvent);
console.log('Content set successfully, final value:', editor.innerText);
return true;
}
return setEditorContent('%s');
`, strings.ReplaceAll(fullContent, "'", "\\'"))
result, err := p.Page.Eval(jsScript)
if err != nil {
return false, err.Error()
}
if result != nil && result.Value.Bool() {
p.LogInfo("✅ 通过JS终极方案成功设置内容")
p.SleepMs(1000)
return true, "内容输入成功"
}
return false, "未找到编辑器元素"
}
func (p *ShipinhaoVideoPublisher) clickPublish() (bool, string) {
p.LogInfo("点击发布按钮...")
p.Page.Eval(`() => window.scrollTo(0, document.body.scrollHeight)`)
p.SleepMs(1000)
p.ensureInEditorIframe()
p.SleepMs(500)
publishScript := `
var buttons = document.querySelectorAll('button');
for (var i = 0; i < buttons.length; i++) {
var btn = buttons[i];
var text = btn.innerText || btn.textContent || '';
if (text.trim() === '发表' && btn.offsetParent !== null) {
btn.click();
return true;
}
}
return false;
`
result, err := p.Page.Eval(publishScript)
if err == nil && result != nil && result.Value.Bool() {
p.LogInfo("✅ 已点击发表按钮")
return true, "已点击发表"
}
publishSelectors := []string{
".weui-desktop-btn.weui-desktop-btn_primary",
"button.weui-desktop-btn_primary",
".weui-desktop-btn_wrp button",
"button[class*='primary']",
}
for _, selector := range publishSelectors {
btns, err := p.Page.Elements(selector)
if err == nil {
for _, btn := range btns {
visible, _ := btn.Visible()
if visible {
text, _ := btn.Text()
if text == "发表" || strings.Contains(text, "发表") {
p.JSClick(btn)
p.LogInfo(fmt.Sprintf("✅ 通过选择器 %s 点击发表按钮", selector))
return true, "已点击发表"
}
}
}
}
}
xpaths := []string{
"//button[contains(text(), '发表')]",
"//button[contains(@class, 'primary') and contains(text(), '发表')]",
"//div[contains(@class, 'weui-desktop-btn_wrp')]//button",
}
for _, xpath := range xpaths {
btns, err := p.Page.ElementsX(xpath)
if err == nil {
for _, btn := range btns {
visible, _ := btn.Visible()
if visible {
p.JSClick(btn)
p.LogInfo(fmt.Sprintf("✅ 通过XPath %s 点击发表按钮", xpath))
return true, "已点击发表"
}
}
}
}
p.LogError("❌ 所有方法都未找到发表按钮")
return false, "未找到发表按钮"
}
func (p *ShipinhaoVideoPublisher) InitPage() error {
p.Page.MustNavigate(p.EditorURL)
p.Sleep(5)
if err := p.LoadCookies(); err == nil {
p.RefreshPage()
p.Sleep(3)
if !p.CheckLoginStatus() {
return fmt.Errorf("需要登录")
}
p.LogInfo("登录成功")
}
p.SaveCookies()
return nil
}
func (p *ShipinhaoVideoPublisher) 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},
{"确保在iframe", func() error { return p.ensureInEditorIframe() }},
}
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(2000)
filePath, _ := filepath.Abs(p.SourcePath)
uploadSuccess := false
uploadMessage := ""
methods := []struct {
name string
fn func(string) (bool, string)
}{
{"CDP拦截上传", p.uploadViaCdpIntercept},
{"拖拽事件上传", p.uploadViaDragEvent},
{"React事件上传", p.uploadViaReactEvent},
}
for _, method := range methods {
p.LogInfo(fmt.Sprintf("尝试 %s...", method.name))
uploadSuccess, uploadMessage = method.fn(filePath)
if uploadSuccess {
p.LogInfo(fmt.Sprintf("%s 成功", method.name))
break
}
p.LogWarning(fmt.Sprintf("%s 失败: %s", method.name, uploadMessage))
p.SleepMs(1000)
}
if !uploadSuccess {
return false, fmt.Sprintf("所有上传方法均失败: %s", uploadMessage)
}
p.waitForUploadComplete(180)
p.inputTitleAndDescription()
success, message := p.clickPublish()
if !success {
return false, message
}
p.Sleep(10)
currentURL := p.GetCurrentURL()
if strings.Contains(currentURL, "https://channels.weixin.qq.com/platform/post/list") {
p.LogInfo("🎉 发布完成")
return true, "发布成功"
}
return false, "发布失败"
}
func (p *ShipinhaoVideoPublisher) LogWarning(message string) {
p.Logger.Printf("⚠️ %s", message)
}