583 lines
15 KiB
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)
|
|
}
|