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) }