640 lines
18 KiB
Go
640 lines
18 KiB
Go
package publisher
|
||
|
||
import (
|
||
"context"
|
||
"encoding/base64"
|
||
"fmt"
|
||
"geo/internal/config"
|
||
"log"
|
||
"os"
|
||
"path/filepath"
|
||
"strings"
|
||
"time"
|
||
)
|
||
|
||
// ShipinhaoVideoPublisher 视频号视频发布器
|
||
type ShipinhaoVideoPublisher struct {
|
||
*BasePublisher
|
||
}
|
||
|
||
// NewShipinhaoVideoPublisher 创建视频号发布器
|
||
func NewShipinhaoVideoPublisher(ctx context.Context, task *TaskParams, config *config.Config, logger *log.Logger) PublisherInerface {
|
||
return &ShipinhaoVideoPublisher{
|
||
BasePublisher: NewBasePublisher(ctx, task, config, logger),
|
||
}
|
||
}
|
||
|
||
// PublishNote 发布视频主流程
|
||
func (p *ShipinhaoVideoPublisher) PublishNote() (bool, string) {
|
||
p.StartNote()
|
||
|
||
// 1. 初始化浏览器
|
||
if err := p.SetupDriver(); err != nil {
|
||
return false, fmt.Sprintf("浏览器启动失败: %v", err)
|
||
}
|
||
defer p.Page.Close()
|
||
// 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},
|
||
//{"拖拽事件上传", p.uploadViaDragEvent},
|
||
//{"网络拦截上传", p.uploadViaNetworkIntercept},
|
||
//{"React事件上传", p.uploadViaReactEvent},
|
||
//{"文件输入框上传", p.uploadViaFileInput},
|
||
}
|
||
|
||
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. 等待发布完成
|
||
p.Sleep(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.Page = frame
|
||
p.LogInfo(fmt.Sprintf("已切换到 iframe: %s", selector))
|
||
return
|
||
}
|
||
}
|
||
}
|
||
|
||
p.LogInfo("未找到 iframe,使用主页面")
|
||
}
|
||
|
||
// uploadViaFileInput 通过文件输入框上传(最基础的方法)
|
||
func (p *ShipinhaoVideoPublisher) uploadViaFileInput(filePath string) error {
|
||
p.LogInfo("使用文件输入框上传...")
|
||
|
||
// 确保在正确的 iframe 中
|
||
p.ensureInEditorIframe()
|
||
|
||
// 查找文件输入框(使用非阻塞方式)
|
||
fileInputs, err := p.Page.Elements("input[type='file']")
|
||
if err != nil {
|
||
return fmt.Errorf("查找文件输入框失败: %v", err)
|
||
}
|
||
|
||
if len(fileInputs) == 0 {
|
||
return fmt.Errorf("未找到文件输入框")
|
||
}
|
||
|
||
err = fileInputs[0].SetFiles([]string{filePath})
|
||
if err != nil {
|
||
return fmt.Errorf("设置文件失败: %v", err)
|
||
}
|
||
|
||
p.LogInfo(fmt.Sprintf("文件已选择: %s", filepath.Base(filePath)))
|
||
return nil
|
||
}
|
||
|
||
// uploadViaCDPIntercept 使用 CDP 拦截并注入文件上传
|
||
func (p *ShipinhaoVideoPublisher) uploadViaCDPIntercept() error {
|
||
p.LogInfo("使用 CDP 协议拦截文件上传...")
|
||
|
||
// 确保在正确的 iframe 中
|
||
p.ensureInEditorIframe()
|
||
p.LogInfo("已切换到 iframe,开始查找文件输入框")
|
||
p.Sleep(1)
|
||
|
||
// 先在当前 iframe 中查找文件输入框
|
||
fileInputs, err := p.Page.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))
|
||
}
|
||
//filePath := p.SourcePath
|
||
//// 读取文件为 Base64
|
||
//fileData, err := os.ReadFile(filePath)
|
||
//if err != nil {
|
||
// return fmt.Errorf("读取文件失败: %v", err)
|
||
//}
|
||
//base64Data := base64.StdEncoding.EncodeToString(fileData)
|
||
//fileName := filepath.Base(filePath)
|
||
//// 如果直接设置失败,使用 JS 注入方式
|
||
//p.LogInfo("使用 JS 注入方式上传文件")
|
||
//
|
||
//// 注入 JS 代码模拟文件上传
|
||
//script := fmt.Sprintf(`
|
||
// (function() {
|
||
// // 创建 File 对象
|
||
// 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"][accept*="video"]');
|
||
// if (!fileInput) {
|
||
// fileInput = document.querySelector('input[type="file"]');
|
||
// }
|
||
//
|
||
// if (!fileInput) {
|
||
// // 如果还是找不到,创建一个
|
||
// fileInput = document.createElement('input');
|
||
// fileInput.type = 'file';
|
||
// fileInput.accept = 'video/mp4,video/x-m4v,video/*';
|
||
// fileInput.multiple = true;
|
||
// fileInput.style.display = 'none';
|
||
// document.body.appendChild(fileInput);
|
||
// }
|
||
//
|
||
// // 临时显示文件输入框(如果需要)
|
||
// var originalDisplay = fileInput.style.display;
|
||
// fileInput.style.display = 'block';
|
||
//
|
||
// // 使用 DataTransfer 设置文件
|
||
// var dataTransfer = new DataTransfer();
|
||
// dataTransfer.items.add(file);
|
||
// fileInput.files = dataTransfer.files;
|
||
//
|
||
// // 恢复原始显示状态
|
||
// fileInput.style.display = originalDisplay;
|
||
//
|
||
// // 触发 change 事件
|
||
// var changeEvent = new Event('change', { bubbles: true });
|
||
// fileInput.dispatchEvent(changeEvent);
|
||
//
|
||
// // 触发 input 事件
|
||
// var inputEvent = new Event('input', { bubbles: true });
|
||
// fileInput.dispatchEvent(inputEvent);
|
||
//
|
||
// // 查找上传区域并触发点击
|
||
// var uploadArea = document.querySelector('.upload-wrap, .video-plugin-title-action, [class*="upload"]');
|
||
// if (uploadArea) {
|
||
// uploadArea.click();
|
||
// }
|
||
//
|
||
// // 模拟拖拽事件
|
||
// var dropZones = document.querySelectorAll('.upload-wrap, [class*="upload"], [class*="drop"]');
|
||
// for (var i = 0; i < dropZones.length; i++) {
|
||
// var zone = dropZones[i];
|
||
// if (zone.offsetParent !== null) {
|
||
// var dragOverEvent = new DragEvent('dragover', {
|
||
// bubbles: true,
|
||
// cancelable: true,
|
||
// dataTransfer: dataTransfer
|
||
// });
|
||
// zone.dispatchEvent(dragOverEvent);
|
||
//
|
||
// var dropEvent = new DragEvent('drop', {
|
||
// bubbles: true,
|
||
// cancelable: true,
|
||
// dataTransfer: dataTransfer
|
||
// });
|
||
// zone.dispatchEvent(dropEvent);
|
||
// break;
|
||
// }
|
||
// }
|
||
//
|
||
// return {success: true, fileName: '%s', hasFileInput: !!fileInput};
|
||
// })();
|
||
//`, base64Data, fileName, fileName)
|
||
//
|
||
//result, err := p.Page.Eval(script)
|
||
//if err != nil {
|
||
// return fmt.Errorf("CDP 注入失败: %v", err)
|
||
//}
|
||
//
|
||
//p.LogInfo(fmt.Sprintf("CDP 注入完成,结果: %v", result))
|
||
return nil
|
||
}
|
||
|
||
// uploadViaDragEvent 通过模拟拖拽事件上传
|
||
func (p *ShipinhaoVideoPublisher) uploadViaDragEvent(filePath string) error {
|
||
p.LogInfo("模拟拖拽事件上传...")
|
||
|
||
// 读取文件为 Base64
|
||
fileData, err := os.ReadFile(filePath)
|
||
if err != nil {
|
||
return fmt.Errorf("读取文件失败: %v", err)
|
||
}
|
||
base64Data := base64.StdEncoding.EncodeToString(fileData)
|
||
fileName := filepath.Base(filePath)
|
||
|
||
// 确保在正确的 iframe 中
|
||
p.ensureInEditorIframe()
|
||
p.Sleep(1)
|
||
|
||
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: '拖拽事件已触发'};
|
||
})();
|
||
`, base64Data, fileName)
|
||
|
||
_, err = p.Page.Eval(script)
|
||
if err != nil {
|
||
return fmt.Errorf("拖拽事件上传失败: %v", err)
|
||
}
|
||
|
||
p.LogInfo("拖拽事件已触发")
|
||
return nil
|
||
}
|
||
|
||
// uploadViaNetworkIntercept 通过拦截网络请求上传
|
||
func (p *ShipinhaoVideoPublisher) uploadViaNetworkIntercept(filePath string) error {
|
||
p.LogInfo("尝试网络拦截上传...")
|
||
|
||
// 确保在正确的 iframe 中
|
||
p.ensureInEditorIframe()
|
||
|
||
// 点击上传区域
|
||
clickScript := `
|
||
var areas = document.querySelectorAll('[class*="upload"]');
|
||
for (var i = 0; i < areas.length; i++) {
|
||
if (areas[i].offsetParent !== null) {
|
||
areas[i].click();
|
||
return true;
|
||
}
|
||
}
|
||
return false;
|
||
`
|
||
_, err := p.Page.Eval(clickScript)
|
||
if err != nil {
|
||
p.LogInfo(fmt.Sprintf("点击上传区域失败: %v", err))
|
||
}
|
||
p.Sleep(1)
|
||
|
||
// 使用 CDP 设置文件输入
|
||
fileInputs, err := p.Page.Elements("input[type='file']")
|
||
if err != nil || len(fileInputs) == 0 {
|
||
return fmt.Errorf("未找到文件输入框")
|
||
}
|
||
|
||
err = fileInputs[0].SetFiles([]string{filePath})
|
||
if err != nil {
|
||
return fmt.Errorf("设置文件失败: %v", err)
|
||
}
|
||
|
||
p.LogInfo("网络拦截上传完成")
|
||
return nil
|
||
}
|
||
|
||
// uploadViaReactEvent 通过 React 内部事件上传
|
||
func (p *ShipinhaoVideoPublisher) uploadViaReactEvent(filePath string) error {
|
||
p.LogInfo("尝试 React 事件上传...")
|
||
|
||
// 读取文件为 Base64
|
||
fileData, err := os.ReadFile(filePath)
|
||
if err != nil {
|
||
return fmt.Errorf("读取文件失败: %v", err)
|
||
}
|
||
base64Data := base64.StdEncoding.EncodeToString(fileData)
|
||
fileName := filepath.Base(filePath)
|
||
|
||
// 确保在正确的 iframe 中
|
||
p.ensureInEditorIframe()
|
||
|
||
script := fmt.Sprintf(`
|
||
(function() {
|
||
// 查找所有 DOM 元素
|
||
var allElements = document.querySelectorAll('*');
|
||
var uploadComponent = null;
|
||
|
||
for (var i = 0; i < allElements.length; i++) {
|
||
var el = allElements[i];
|
||
// 检查是否有 React 内部属性
|
||
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);
|
||
|
||
// 触发 change 事件
|
||
var fileInput = document.querySelector('input[type="file"]');
|
||
if (fileInput) {
|
||
fileInput.files = dataTransfer.files;
|
||
var event = new Event('change', {bubbles: true});
|
||
fileInput.dispatchEvent(event);
|
||
}
|
||
|
||
// 尝试触发 React 的 onChange
|
||
var syntheticEvent = new Event('change', {bubbles: true});
|
||
syntheticEvent.target = {files: dataTransfer.files};
|
||
uploadComponent.dispatchEvent(syntheticEvent);
|
||
|
||
return {success: true};
|
||
}
|
||
|
||
return {success: false, message: '未找到 React 组件'};
|
||
})();
|
||
`, base64Data, fileName)
|
||
|
||
result, err := p.Page.Eval(script)
|
||
if err != nil {
|
||
return fmt.Errorf("React 事件上传失败: %v", err)
|
||
}
|
||
|
||
// 检查结果
|
||
if result != nil {
|
||
p.LogInfo(fmt.Sprintf("React 事件触发结果: %v", result))
|
||
}
|
||
|
||
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.Page.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 中
|
||
p.ensureInEditorIframe()
|
||
p.Sleep(1)
|
||
|
||
// 使用 JavaScript 直接设置编辑器内容
|
||
script := `
|
||
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);
|
||
});
|
||
|
||
// 尝试触发 React 的合成事件
|
||
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);
|
||
}
|
||
|
||
console.log('Content set successfully, final value:', editor.innerText);
|
||
return true;
|
||
}
|
||
return setEditorContent(arguments[0]);
|
||
`
|
||
|
||
result, err := p.Page.Eval(script, fullContent)
|
||
if err != nil {
|
||
return false, fmt.Sprintf("设置内容失败: %v", err)
|
||
}
|
||
|
||
if result != nil {
|
||
p.LogInfo("通过 JS 成功设置内容")
|
||
p.Sleep(1)
|
||
return true, "内容输入成功"
|
||
}
|
||
|
||
return false, "未找到编辑器元素"
|
||
}
|
||
|
||
// clickPublish 点击发布按钮
|
||
func (p *ShipinhaoVideoPublisher) clickPublish() (bool, string) {
|
||
p.LogInfo("点击发布按钮...")
|
||
|
||
// 滚动到底部
|
||
p.Page.Eval(`window.scrollTo(0, document.body.scrollHeight);`)
|
||
p.Sleep(1)
|
||
|
||
// 确保在正确的 iframe 中
|
||
p.ensureInEditorIframe()
|
||
p.Sleep(1)
|
||
|
||
// 方法1: 通过文本 "发表" 查找按钮
|
||
script := `
|
||
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(script)
|
||
if err == nil && result != nil {
|
||
p.LogInfo("已点击发表按钮")
|
||
return true, "已点击发表"
|
||
}
|
||
|
||
// 方法2: 通过 CSS 选择器查找
|
||
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 {
|
||
btn, err := p.Page.Element(selector)
|
||
if err == nil && btn != nil {
|
||
visible, _ := btn.Visible()
|
||
if visible {
|
||
p.JSClick(btn)
|
||
p.LogInfo(fmt.Sprintf("通过选择器 %s 点击发表按钮", selector))
|
||
return true, "已点击发表"
|
||
}
|
||
}
|
||
}
|
||
|
||
p.LogError("所有方法都未找到发表按钮")
|
||
return false, "未找到发表按钮"
|
||
}
|