This commit is contained in:
parent
38e1995f86
commit
e2aad42e81
|
|
@ -46,7 +46,10 @@ func (b *PublishBiz) ValidateAccessToken(ctx context.Context, accessToken string
|
||||||
And(builder.Eq{"status": 1})
|
And(builder.Eq{"status": 1})
|
||||||
tokenInfo := &model.Token{}
|
tokenInfo := &model.Token{}
|
||||||
err := b.tokenImpl.GetOneBySearchStruct(ctx, &cond, tokenInfo)
|
err := b.tokenImpl.GetOneBySearchStruct(ctx, &cond, tokenInfo)
|
||||||
if err != nil || tokenInfo == nil {
|
if err != nil {
|
||||||
|
return nil, errcode.Forbidden("密钥无效或已禁用")
|
||||||
|
}
|
||||||
|
if tokenInfo.ID == 0 {
|
||||||
return nil, errcode.Forbidden("密钥无效或已禁用")
|
return nil, errcode.Forbidden("密钥无效或已禁用")
|
||||||
}
|
}
|
||||||
return tokenInfo, nil
|
return tokenInfo, nil
|
||||||
|
|
|
||||||
|
|
@ -201,8 +201,8 @@ func (p *PublishImpl) GetListWithUser(ctx context.Context, tokenID int32, page,
|
||||||
p.create_time,
|
p.create_time,
|
||||||
p.msg
|
p.msg
|
||||||
`).
|
`).
|
||||||
Joins("LEFT JOIN user u ON p.user_index = u.user_index").
|
Joins("LEFT JOIN user u ON p.user_index COLLATE utf8mb4_unicode_ci = u.user_index").
|
||||||
Joins("LEFT JOIN plat pl ON p.plat_index = pl.index").
|
Joins("LEFT JOIN plat pl ON p.plat_index COLLATE utf8mb4_unicode_ci = pl.index").
|
||||||
Where("u.token_id = ?", tokenID)
|
Where("u.token_id = ?", tokenID)
|
||||||
|
|
||||||
// 添加过滤条件
|
// 添加过滤条件
|
||||||
|
|
|
||||||
|
|
@ -450,7 +450,7 @@ func (pm *PublishManager) extractTaskParams(publishData *entitys.PublishTaskDeta
|
||||||
func (pm *PublishManager) downloadAndPrepareFiles(requestId string, params *fileUrl, taskLogger *log.Logger, publishClass *publisher.PublisherValue) (docPath, imgPath string, err error) {
|
func (pm *PublishManager) downloadAndPrepareFiles(requestId string, params *fileUrl, taskLogger *log.Logger, publishClass *publisher.PublisherValue) (docPath, imgPath string, err error) {
|
||||||
// 下载文档
|
// 下载文档
|
||||||
taskLogger.Printf("[任务 %s] 开始下载文档...", requestId)
|
taskLogger.Printf("[任务 %s] 开始下载文档...", requestId)
|
||||||
docPath, err = pkg.DownloadFile(params.url, pm.Conf.Sys.DocsDir, requestId+".docx")
|
docPath, err = pkg.DownloadFile(params.url, pm.Conf.Sys.DocsDir, requestId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", "", fmt.Errorf("下载文档失败: %v", err)
|
return "", "", fmt.Errorf("下载文档失败: %v", err)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -104,13 +104,9 @@ func (p *BaijiahaoPublisher) PublishNote() (bool, string) {
|
||||||
}
|
}
|
||||||
driverCreated = true
|
driverCreated = true
|
||||||
|
|
||||||
p.Page.MustNavigate(p.EditorURL)
|
|
||||||
p.Sleep(3)
|
|
||||||
p.WaitForPageReady(5)
|
|
||||||
|
|
||||||
if p.LoadCookies() == nil {
|
if p.LoadCookies() == nil {
|
||||||
p.RefreshPage()
|
p.Page.MustNavigate(p.EditorURL)
|
||||||
p.Sleep(3)
|
p.WaitForPageReady(5)
|
||||||
if p.CheckLoginStatus() {
|
if p.CheckLoginStatus() {
|
||||||
return p.doPublish()
|
return p.doPublish()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -127,6 +127,9 @@ func (b *BasePublisher) SetupDriver() error {
|
||||||
os.MkdirAll(userDataDir, 0755)
|
os.MkdirAll(userDataDir, 0755)
|
||||||
l.UserDataDir(userDataDir)
|
l.UserDataDir(userDataDir)
|
||||||
|
|
||||||
|
// 关键优化:不重新使用已有的数据目录时不要清除
|
||||||
|
l.Set("profile-directory", "Default")
|
||||||
|
|
||||||
// 设置 Leakless 模式(解决 Windows 上的问题)
|
// 设置 Leakless 模式(解决 Windows 上的问题)
|
||||||
l.Leakless(false)
|
l.Leakless(false)
|
||||||
|
|
||||||
|
|
@ -294,9 +297,13 @@ func (b *BasePublisher) WaitLogin() (bool, string) {
|
||||||
return false, "需要实现"
|
return false, "需要实现"
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *BasePublisher) CheckLoginStatus() bool {
|
func (p *BasePublisher) CheckLoginStatus() bool {
|
||||||
|
currentURL := p.GetCurrentURL()
|
||||||
|
if strings.Contains(currentURL, p.LoginURL) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
func (b *BasePublisher) CheckLogin() (bool, string) {
|
func (b *BasePublisher) CheckLogin() (bool, string) {
|
||||||
return false, "需要实现"
|
return false, "需要实现"
|
||||||
|
|
@ -354,3 +361,21 @@ func (b *BasePublisher) StartNote() {
|
||||||
b.LogInfo(fmt.Sprintf("标签: %v", b.Tags))
|
b.LogInfo(fmt.Sprintf("标签: %v", b.Tags))
|
||||||
b.LogInfo(strings.Repeat("=", 50))
|
b.LogInfo(strings.Repeat("=", 50))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// InitPage 初始化页面
|
||||||
|
func (p *BasePublisher) InitPage() error {
|
||||||
|
|
||||||
|
// 尝试加载cookies并检查登录状态
|
||||||
|
if err := p.LoadCookies(); err == nil {
|
||||||
|
p.Page.MustNavigate(p.EditorURL)
|
||||||
|
p.WaitForPageReady(5)
|
||||||
|
p.Sleep(2)
|
||||||
|
}
|
||||||
|
// 统一检查登录状态
|
||||||
|
if !p.CheckLoginStatus() {
|
||||||
|
p.LogInfo("未登录或登录已过期,需要重新登录")
|
||||||
|
return fmt.Errorf("需要登录")
|
||||||
|
}
|
||||||
|
p.SaveCookies()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,220 @@
|
||||||
|
package publisher
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"geo/internal/config"
|
||||||
|
"log"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/go-rod/rod"
|
||||||
|
)
|
||||||
|
|
||||||
|
type CSDNPublisher struct {
|
||||||
|
*BasePublisher
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewCSDNPublisher(task *TaskParams, cfg *config.Config, logger *log.Logger) PublisherInerface {
|
||||||
|
return &CSDNPublisher{NewBasePublisher(task, cfg, logger)}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *CSDNPublisher) CheckLogin() (bool, string) {
|
||||||
|
p.LogInfo("检查登录状态...")
|
||||||
|
|
||||||
|
if err := p.SetupDriver(); err != nil {
|
||||||
|
return false, fmt.Sprintf("浏览器启动失败: %v", err)
|
||||||
|
}
|
||||||
|
defer p.Close()
|
||||||
|
|
||||||
|
p.Page.MustNavigate(p.EditorURL)
|
||||||
|
p.Sleep(2)
|
||||||
|
p.WaitForPageReady(3)
|
||||||
|
|
||||||
|
if p.CheckLoginStatus() {
|
||||||
|
p.SaveCookies()
|
||||||
|
return true, "已登录"
|
||||||
|
}
|
||||||
|
return false, "未登录"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *CSDNPublisher) WaitLogin() (bool, string) {
|
||||||
|
p.LogInfo("开始等待登录...")
|
||||||
|
|
||||||
|
if err := p.SetupDriver(); err != nil {
|
||||||
|
return false, fmt.Sprintf("浏览器启动失败: %v", err)
|
||||||
|
}
|
||||||
|
defer p.Close()
|
||||||
|
|
||||||
|
p.Page.MustNavigate(p.EditorURL)
|
||||||
|
p.Sleep(3)
|
||||||
|
|
||||||
|
if p.CheckLoginStatus() {
|
||||||
|
p.SaveCookies()
|
||||||
|
p.LogInfo("已有登录状态")
|
||||||
|
return true, "already_logged_in"
|
||||||
|
}
|
||||||
|
|
||||||
|
startTime := time.Now()
|
||||||
|
timeout := 120
|
||||||
|
for time.Since(startTime) < time.Duration(timeout)*time.Second {
|
||||||
|
time.Sleep(1 * time.Second)
|
||||||
|
if p.CheckLoginStatus() {
|
||||||
|
p.SaveCookies()
|
||||||
|
p.LogInfo("登录成功")
|
||||||
|
return true, "login_success"
|
||||||
|
}
|
||||||
|
p.SleepMs(1000)
|
||||||
|
}
|
||||||
|
|
||||||
|
return false, "登录超时,请检查网络或账号状态"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *CSDNPublisher) inputTitle() error {
|
||||||
|
p.LogInfo("输入文章标题...")
|
||||||
|
|
||||||
|
titleInput, err := p.WaitForElementVisible("#txtTitle", 5)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("未找到标题输入框: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
p.LogInfo("找到标题输入框")
|
||||||
|
|
||||||
|
if err := p.ClearInput(titleInput); err != nil {
|
||||||
|
titleInput.Input("")
|
||||||
|
}
|
||||||
|
p.SleepMs(200)
|
||||||
|
|
||||||
|
if err := p.SetInputValue(titleInput, p.Title); err != nil {
|
||||||
|
titleInput.Input(p.Title)
|
||||||
|
}
|
||||||
|
|
||||||
|
p.LogInfo(fmt.Sprintf("标题已输入: %s", p.Title))
|
||||||
|
|
||||||
|
p.SleepMs(300)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *CSDNPublisher) inputContent(titleInput *rod.Element) error {
|
||||||
|
p.LogInfo("输入文章内容...")
|
||||||
|
|
||||||
|
p.SleepMs(1000)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *CSDNPublisher) clickPublish() error {
|
||||||
|
p.LogInfo("点击发布按钮...")
|
||||||
|
|
||||||
|
btnBox, err := p.WaitForElementVisible("div.btn-box", 3)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("未找到发布按钮区域: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
publishBtn, err := btnBox.ElementX(".//button[contains(@class, 'btn-outline-danger') and .//span[text()='发布博客']]")
|
||||||
|
if err != nil {
|
||||||
|
publishBtn, err = p.Page.ElementX("//button[contains(@class, 'btn-outline-danger') and .//span[text()='发布博客']]")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("未找到发布按钮: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := publishBtn.ScrollIntoView(); err != nil {
|
||||||
|
p.LogInfo(fmt.Sprintf("滚动到发布按钮失败: %v", err))
|
||||||
|
}
|
||||||
|
p.SleepMs(300)
|
||||||
|
|
||||||
|
if err := p.JSClick(publishBtn); err != nil {
|
||||||
|
return fmt.Errorf("点击发布按钮失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
p.LogInfo("已点击发布按钮")
|
||||||
|
p.SleepMs(2000)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *CSDNPublisher) waitForPublishResult() (bool, string) {
|
||||||
|
p.LogInfo("等待发布结果...")
|
||||||
|
|
||||||
|
for attempt := 0; attempt < 10; attempt++ {
|
||||||
|
currentURL := p.GetCurrentURL()
|
||||||
|
p.LogInfo(fmt.Sprintf("第 %d 次检查 - URL: %s", attempt+1, currentURL))
|
||||||
|
|
||||||
|
if strings.Contains(currentURL, "success") {
|
||||||
|
p.LogInfo(fmt.Sprintf("发布成功!URL: %s", currentURL))
|
||||||
|
return true, "发布成功"
|
||||||
|
}
|
||||||
|
|
||||||
|
errorSelectors := []string{
|
||||||
|
"[class*='el_mcm-message--error'] .el_mcm-message__content",
|
||||||
|
"[class*='el_mcm-message--error']",
|
||||||
|
"[class*='message--error']",
|
||||||
|
".error",
|
||||||
|
".alert-error",
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, selector := range errorSelectors {
|
||||||
|
errorElements, _ := p.Page.Elements(selector)
|
||||||
|
for _, elem := range errorElements {
|
||||||
|
visible, _ := elem.Visible()
|
||||||
|
if visible {
|
||||||
|
text, _ := elem.Text()
|
||||||
|
if text != "" {
|
||||||
|
p.LogError(fmt.Sprintf("发布失败: %s", text))
|
||||||
|
return false, fmt.Sprintf("发布失败: %s", text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 截图保存用于调试
|
||||||
|
if attempt == 1 || attempt == 9 {
|
||||||
|
screenshotPath := fmt.Sprintf("debug_%s_%d.png", p.RequestID, attempt)
|
||||||
|
p.Screenshot(screenshotPath)
|
||||||
|
p.LogInfo(fmt.Sprintf("已保存截图: %s", screenshotPath))
|
||||||
|
}
|
||||||
|
|
||||||
|
p.SleepMs(1000)
|
||||||
|
}
|
||||||
|
|
||||||
|
return false, "发布结果未知"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *CSDNPublisher) 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},
|
||||||
|
{"保存cookie", p.SaveCookies},
|
||||||
|
{"输入标题", p.inputTitle},
|
||||||
|
{"输入内容", func() error {
|
||||||
|
titleInput, _ := p.WaitForElementVisible("#txtTitle", 5)
|
||||||
|
return p.inputContent(titleInput)
|
||||||
|
}},
|
||||||
|
{"点击发布", p.clickPublish},
|
||||||
|
}
|
||||||
|
|
||||||
|
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(500)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 等待发布结果
|
||||||
|
return p.waitForPublishResult()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *CSDNPublisher) LogWarning(message string) {
|
||||||
|
p.Logger.Printf("⚠️ %s", message)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,427 @@
|
||||||
|
package publisher
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"geo/internal/config"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/go-rod/rod"
|
||||||
|
"github.com/go-rod/rod/lib/proto"
|
||||||
|
)
|
||||||
|
|
||||||
|
type DouyinSpPublisher struct {
|
||||||
|
*BasePublisher
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewDouyinSpPublisher(task *TaskParams, cfg *config.Config, logger *log.Logger) PublisherInerface {
|
||||||
|
return &DouyinSpPublisher{NewBasePublisher(task, cfg, logger)}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *DouyinSpPublisher) CheckLogin() (bool, string) {
|
||||||
|
p.LogInfo("检查登录状态...")
|
||||||
|
|
||||||
|
if err := p.SetupDriver(); err != nil {
|
||||||
|
return false, fmt.Sprintf("浏览器启动失败: %v", err)
|
||||||
|
}
|
||||||
|
defer p.Close()
|
||||||
|
|
||||||
|
p.Page.MustNavigate(p.EditorURL)
|
||||||
|
p.Sleep(3)
|
||||||
|
p.WaitForPageReady(5)
|
||||||
|
|
||||||
|
if p.CheckLoginStatus() {
|
||||||
|
p.SaveCookies()
|
||||||
|
return true, "已登录"
|
||||||
|
}
|
||||||
|
return false, "未登录"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *DouyinSpPublisher) CheckLoginStatus() bool {
|
||||||
|
currentURL := p.GetCurrentURL()
|
||||||
|
if strings.Contains(currentURL, p.LoginedURL) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if strings.Contains(currentURL, p.EditorURL) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *DouyinSpPublisher) 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(2)
|
||||||
|
|
||||||
|
if p.CheckLoginStatus() {
|
||||||
|
p.SaveCookies()
|
||||||
|
return true, "already_logged_in"
|
||||||
|
}
|
||||||
|
|
||||||
|
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(1000)
|
||||||
|
}
|
||||||
|
|
||||||
|
return false, "登录超时,请检查网络或账号状态"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *DouyinSpPublisher) waitForEditorReady(timeout int) bool {
|
||||||
|
p.LogInfo("等待编辑器加载...")
|
||||||
|
startTime := time.Now()
|
||||||
|
for time.Since(startTime) < time.Duration(timeout)*time.Second {
|
||||||
|
uploadArea, err := p.Page.Element(".container-drag-icon")
|
||||||
|
if err == nil && uploadArea != nil {
|
||||||
|
p.LogInfo("编辑器加载完成")
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
p.SleepMs(1000)
|
||||||
|
}
|
||||||
|
p.LogInfo("编辑器加载超时")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *DouyinSpPublisher) uploadVideo() error {
|
||||||
|
if p.SourcePath == "" {
|
||||||
|
return fmt.Errorf("视频不存在")
|
||||||
|
}
|
||||||
|
if _, err := os.Stat(p.SourcePath); os.IsNotExist(err) {
|
||||||
|
return fmt.Errorf("视频不存在")
|
||||||
|
}
|
||||||
|
|
||||||
|
p.LogInfo(fmt.Sprintf("开始上传视频: %s", p.SourcePath))
|
||||||
|
|
||||||
|
fileInputSelectors := []string{
|
||||||
|
"input[type='file'][accept*='video']",
|
||||||
|
".container-drag-VAfIfu input[type='file']",
|
||||||
|
"input[accept*='video']",
|
||||||
|
}
|
||||||
|
|
||||||
|
var fileInput *rod.Element
|
||||||
|
for _, selector := range fileInputSelectors {
|
||||||
|
fileInput, _ = p.Page.Element(selector)
|
||||||
|
if fileInput != nil {
|
||||||
|
p.LogInfo(fmt.Sprintf("找到文件上传输入框: %s", selector))
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if fileInput == nil {
|
||||||
|
uploadArea, err := p.WaitForElementVisible(".container-drag-VAfIfu", 5)
|
||||||
|
if err == nil && uploadArea != nil {
|
||||||
|
p.LogInfo("点击上传区域")
|
||||||
|
p.JSClick(uploadArea)
|
||||||
|
p.SleepMs(1000)
|
||||||
|
|
||||||
|
for _, selector := range fileInputSelectors {
|
||||||
|
fileInput, _ = p.Page.Element(selector)
|
||||||
|
if fileInput != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if fileInput == nil {
|
||||||
|
return fmt.Errorf("未找到文件上传输入框")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := fileInput.SetFiles([]string{p.SourcePath}); err != nil {
|
||||||
|
return fmt.Errorf("上传视频失败: %v", err)
|
||||||
|
}
|
||||||
|
p.LogInfo(fmt.Sprintf("视频文件已选择: %s", p.SourcePath))
|
||||||
|
|
||||||
|
return p.waitForUploadComplete()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *DouyinSpPublisher) waitForUploadComplete() error {
|
||||||
|
p.LogInfo("等待视频上传完成...")
|
||||||
|
|
||||||
|
for i := 0; i < 300; i++ {
|
||||||
|
successElements, _ := p.Page.Elements(".upload-success, [class*='success']")
|
||||||
|
if len(successElements) > 0 {
|
||||||
|
p.LogInfo("视频上传成功")
|
||||||
|
p.SleepMs(2000)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
errorElements, _ := p.Page.Elements(".upload-error, [class*='error']")
|
||||||
|
for _, elem := range errorElements {
|
||||||
|
visible, _ := elem.Visible()
|
||||||
|
if visible {
|
||||||
|
text, _ := elem.Text()
|
||||||
|
if text != "" {
|
||||||
|
return fmt.Errorf("上传失败: %s", text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
p.SleepMs(1000)
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf("上传超时")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *DouyinSpPublisher) inputTitle() error {
|
||||||
|
p.LogInfo("输入视频标题...")
|
||||||
|
|
||||||
|
titleSelectors := []string{
|
||||||
|
"textarea[placeholder*='标题']",
|
||||||
|
"input[placeholder*='标题']",
|
||||||
|
".container-sGoJ9f input",
|
||||||
|
".semiInput-EyEyPL input",
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, selector := range titleSelectors {
|
||||||
|
titleInput, err := p.WaitForElementVisible(selector, 5)
|
||||||
|
if err == nil && titleInput != nil {
|
||||||
|
p.LogInfo(fmt.Sprintf("找到标题输入框: %s", selector))
|
||||||
|
|
||||||
|
p.ClearInput(titleInput)
|
||||||
|
p.SleepMs(300)
|
||||||
|
titleInput.Input("")
|
||||||
|
p.SleepMs(300)
|
||||||
|
titleInput.Input(p.Title)
|
||||||
|
|
||||||
|
p.LogInfo(fmt.Sprintf("标题已输入: %s", p.Title))
|
||||||
|
p.SleepMs(500)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf("未找到标题输入框")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *DouyinSpPublisher) inputDescription() error {
|
||||||
|
p.LogInfo("输入视频描述...")
|
||||||
|
|
||||||
|
descSelectors := []string{
|
||||||
|
".editor-kit-container",
|
||||||
|
".ProseMirror",
|
||||||
|
"[contenteditable='true']",
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, selector := range descSelectors {
|
||||||
|
descInput, err := p.WaitForElementVisible(selector, 5)
|
||||||
|
if err == nil && descInput != nil {
|
||||||
|
p.LogInfo(fmt.Sprintf("找到描述输入框: %s", selector))
|
||||||
|
|
||||||
|
fullDescription := ""
|
||||||
|
for _, tag := range p.Tags {
|
||||||
|
fullDescription += fmt.Sprintf("#%s ", tag)
|
||||||
|
}
|
||||||
|
fullDescription = strings.TrimSpace(fullDescription)
|
||||||
|
|
||||||
|
p.SetContentEditable(descInput, fullDescription)
|
||||||
|
p.SleepMs(3000)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf("未找到描述输入框")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *DouyinSpPublisher) clickPublish() error {
|
||||||
|
p.LogInfo("点击发布按钮...")
|
||||||
|
|
||||||
|
p.Page.Eval(`() => window.scrollTo(0, document.body.scrollHeight)`)
|
||||||
|
p.SleepMs(2000)
|
||||||
|
|
||||||
|
var publishBtn *rod.Element
|
||||||
|
|
||||||
|
popoverSpan, err := p.WaitForElementVisible("#popover-tip-container", 5)
|
||||||
|
if err == nil && popoverSpan != nil {
|
||||||
|
publishBtn, _ = popoverSpan.Element("button")
|
||||||
|
if publishBtn != nil {
|
||||||
|
text, _ := publishBtn.Text()
|
||||||
|
if text == "发布" {
|
||||||
|
p.LogInfo("通过 popover-tip-container 找到发布按钮")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if publishBtn == nil {
|
||||||
|
publishSelectors := []string{
|
||||||
|
"button:contains('发布')",
|
||||||
|
"button.primary-cECiOJ",
|
||||||
|
"button.primary_button",
|
||||||
|
"button[class*='primary']",
|
||||||
|
}
|
||||||
|
for _, selector := range publishSelectors {
|
||||||
|
publishBtn, _ = p.WaitForElementClickable(selector, 3)
|
||||||
|
if publishBtn != nil {
|
||||||
|
p.LogInfo(fmt.Sprintf("找到发布按钮: %s", selector))
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if publishBtn == nil {
|
||||||
|
p.Page.Eval(`() => window.scrollTo(0, document.body.scrollHeight)`)
|
||||||
|
p.SleepMs(1000)
|
||||||
|
allButtons, _ := p.Page.Elements("button")
|
||||||
|
for _, btn := range allButtons {
|
||||||
|
text, _ := btn.Text()
|
||||||
|
if text == "发布" {
|
||||||
|
publishBtn = btn
|
||||||
|
p.LogInfo("通过遍历按钮找到发布按钮")
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if publishBtn == nil {
|
||||||
|
return fmt.Errorf("未找到发布按钮")
|
||||||
|
}
|
||||||
|
|
||||||
|
p.SleepMs(500)
|
||||||
|
|
||||||
|
if err := publishBtn.Click(proto.InputMouseButtonLeft, 1); err != nil {
|
||||||
|
if err := p.JSClick(publishBtn); err != nil {
|
||||||
|
return fmt.Errorf("点击发布按钮失败: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
p.LogInfo("已点击发布按钮")
|
||||||
|
p.SleepMs(3000)
|
||||||
|
|
||||||
|
confirmSelectors := []string{
|
||||||
|
".semi-modal .semi-button-primary",
|
||||||
|
".confirm-btn-JwJNCk",
|
||||||
|
"button:contains('确认')",
|
||||||
|
"button:contains('确定')",
|
||||||
|
}
|
||||||
|
for _, selector := range confirmSelectors {
|
||||||
|
confirmBtn, _ := p.WaitForElementClickable(selector, 2)
|
||||||
|
if confirmBtn != nil {
|
||||||
|
p.JSClick(confirmBtn)
|
||||||
|
p.LogInfo("已确认发布")
|
||||||
|
p.SleepMs(2000)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *DouyinSpPublisher) waitForPublishResult(timeout int) (bool, string) {
|
||||||
|
p.LogInfo("等待发布结果...")
|
||||||
|
|
||||||
|
startTime := time.Now()
|
||||||
|
for time.Since(startTime) < time.Duration(timeout)*time.Second {
|
||||||
|
currentURL := p.GetCurrentURL()
|
||||||
|
|
||||||
|
successKeywords := []string{"content/manage", "work-management", "success"}
|
||||||
|
for _, keyword := range successKeywords {
|
||||||
|
if strings.Contains(currentURL, keyword) {
|
||||||
|
p.LogInfo(fmt.Sprintf("发布成功!URL: %s", currentURL))
|
||||||
|
return true, "发布成功"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
successSelectors := []string{".semi-toast-content", ".toast-success", "[class*='success']"}
|
||||||
|
for _, selector := range successSelectors {
|
||||||
|
msgs, _ := p.Page.Elements(selector)
|
||||||
|
for _, elem := range msgs {
|
||||||
|
visible, _ := elem.Visible()
|
||||||
|
if visible {
|
||||||
|
text, _ := elem.Text()
|
||||||
|
if strings.Contains(text, "成功") || strings.Contains(text, "已发布") {
|
||||||
|
p.LogInfo(fmt.Sprintf("发布成功: %s", text))
|
||||||
|
return true, text
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
errorSelectors := []string{".semi-toast-content", ".toast-error", "[class*='error']"}
|
||||||
|
for _, selector := range errorSelectors {
|
||||||
|
msgs, _ := p.Page.Elements(selector)
|
||||||
|
for _, elem := range msgs {
|
||||||
|
visible, _ := elem.Visible()
|
||||||
|
if visible {
|
||||||
|
text, _ := elem.Text()
|
||||||
|
if strings.Contains(text, "失败") || strings.Contains(strings.ToLower(text), "error") {
|
||||||
|
p.LogError(fmt.Sprintf("发布失败: %s", text))
|
||||||
|
return false, text
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
p.SleepMs(1000)
|
||||||
|
}
|
||||||
|
|
||||||
|
return false, "发布结果未知(超时)"
|
||||||
|
}
|
||||||
|
|
||||||
|
// InitPage 初始化页面
|
||||||
|
func (p *DouyinSpPublisher) InitPage() error {
|
||||||
|
|
||||||
|
// 尝试加载cookies并检查登录状态
|
||||||
|
if err := p.LoadCookies(); err == nil {
|
||||||
|
p.Page.MustNavigate(p.EditorURL)
|
||||||
|
p.WaitForPageReady(5)
|
||||||
|
p.Sleep(2)
|
||||||
|
}
|
||||||
|
// 统一检查登录状态
|
||||||
|
if !p.CheckLoginStatus() {
|
||||||
|
p.LogInfo("未登录或登录已过期,需要重新登录")
|
||||||
|
return fmt.Errorf("需要登录")
|
||||||
|
}
|
||||||
|
p.SaveCookies()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *DouyinSpPublisher) 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},
|
||||||
|
{"上传视频", p.uploadVideo},
|
||||||
|
{"输入标题", p.inputTitle},
|
||||||
|
{"输入描述", p.inputDescription},
|
||||||
|
{"点击发布", p.clickPublish},
|
||||||
|
}
|
||||||
|
|
||||||
|
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, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
success, message := p.waitForPublishResult(120)
|
||||||
|
if success {
|
||||||
|
p.LogInfo(fmt.Sprintf("🎉 发布成功!%s", message))
|
||||||
|
return true, message
|
||||||
|
}
|
||||||
|
p.LogError(fmt.Sprintf("发布失败: %s", message))
|
||||||
|
return false, message
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *DouyinSpPublisher) LogWarning(message string) {
|
||||||
|
p.Logger.Printf("⚠️ %s", message)
|
||||||
|
}
|
||||||
|
|
@ -49,4 +49,68 @@ var PublisherMap = map[string]*PublisherValue{
|
||||||
Type: 1,
|
Type: 1,
|
||||||
WordContainImg: true,
|
WordContainImg: true,
|
||||||
},
|
},
|
||||||
|
"wyh": {
|
||||||
|
Name: "网易号",
|
||||||
|
InitMethod: NewWangyiPublisher,
|
||||||
|
ContentFormat: "text",
|
||||||
|
ImgNeed: 1,
|
||||||
|
Type: 1,
|
||||||
|
WordContainImg: true,
|
||||||
|
},
|
||||||
|
"shh": {
|
||||||
|
Name: "搜狐号",
|
||||||
|
InitMethod: NewSohuPublisher,
|
||||||
|
ContentFormat: "text",
|
||||||
|
ImgNeed: 1,
|
||||||
|
Type: 1,
|
||||||
|
WordContainImg: false,
|
||||||
|
},
|
||||||
|
"zh": {
|
||||||
|
Name: "知乎",
|
||||||
|
InitMethod: NewZhihuPublisher,
|
||||||
|
ContentFormat: "text",
|
||||||
|
ImgNeed: 1,
|
||||||
|
Type: 1,
|
||||||
|
WordContainImg: true,
|
||||||
|
},
|
||||||
|
"js": {
|
||||||
|
Name: "简书",
|
||||||
|
InitMethod: NewJianshuPublisher,
|
||||||
|
ContentFormat: "markdown",
|
||||||
|
ImgNeed: 1,
|
||||||
|
Type: 1,
|
||||||
|
WordContainImg: false,
|
||||||
|
},
|
||||||
|
"dysp": {
|
||||||
|
Name: "抖音视频",
|
||||||
|
InitMethod: NewDouyinSpPublisher,
|
||||||
|
ContentFormat: "video",
|
||||||
|
ImgNeed: 1,
|
||||||
|
Type: 2,
|
||||||
|
WordContainImg: false,
|
||||||
|
},
|
||||||
|
"xhssp": {
|
||||||
|
Name: "小红书视频",
|
||||||
|
InitMethod: NewXiaohongshuVideoPublisher,
|
||||||
|
ContentFormat: "video",
|
||||||
|
ImgNeed: 1,
|
||||||
|
Type: 2,
|
||||||
|
WordContainImg: false,
|
||||||
|
},
|
||||||
|
"sphsp": {
|
||||||
|
Name: "视频号视频",
|
||||||
|
InitMethod: NewShipinhaoVideoPublisher,
|
||||||
|
ContentFormat: "video",
|
||||||
|
ImgNeed: 1,
|
||||||
|
Type: 2,
|
||||||
|
WordContainImg: false,
|
||||||
|
},
|
||||||
|
"csdn": {
|
||||||
|
Name: "CSDN",
|
||||||
|
InitMethod: NewCSDNPublisher,
|
||||||
|
ContentFormat: "markdown",
|
||||||
|
ImgNeed: 1,
|
||||||
|
Type: 2,
|
||||||
|
WordContainImg: false,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,335 @@
|
||||||
|
package publisher
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"geo/internal/config"
|
||||||
|
"log"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/go-rod/rod"
|
||||||
|
"github.com/go-rod/rod/lib/proto"
|
||||||
|
)
|
||||||
|
|
||||||
|
type JianshuPublisher struct {
|
||||||
|
*BasePublisher
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewJianshuPublisher(task *TaskParams, cfg *config.Config, logger *log.Logger) PublisherInerface {
|
||||||
|
return &JianshuPublisher{NewBasePublisher(task, cfg, logger)}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *JianshuPublisher) CheckLogin() (bool, string) {
|
||||||
|
p.LogInfo("检查登录状态...")
|
||||||
|
|
||||||
|
if err := p.SetupDriver(); err != nil {
|
||||||
|
return false, fmt.Sprintf("浏览器启动失败: %v", err)
|
||||||
|
}
|
||||||
|
defer p.Close()
|
||||||
|
|
||||||
|
p.Page.MustNavigate(p.EditorURL)
|
||||||
|
p.Sleep(3)
|
||||||
|
p.WaitForPageReady(5)
|
||||||
|
|
||||||
|
if p.CheckLoginStatus() {
|
||||||
|
p.SaveCookies()
|
||||||
|
return true, "已登录"
|
||||||
|
}
|
||||||
|
return false, "未登录"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *JianshuPublisher) CheckLoginStatus() bool {
|
||||||
|
currentURL := p.GetCurrentURL()
|
||||||
|
if strings.Contains(currentURL, p.LoginURL) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *JianshuPublisher) WaitLogin() (bool, string) {
|
||||||
|
p.LogInfo("开始等待登录...")
|
||||||
|
|
||||||
|
if err := p.SetupDriver(); err != nil {
|
||||||
|
return false, fmt.Sprintf("浏览器启动失败: %v", err)
|
||||||
|
}
|
||||||
|
defer p.Close()
|
||||||
|
|
||||||
|
p.Page.MustNavigate(p.EditorURL)
|
||||||
|
p.Sleep(3)
|
||||||
|
|
||||||
|
if p.CheckLoginStatus() {
|
||||||
|
p.SaveCookies()
|
||||||
|
return true, "already_logged_in"
|
||||||
|
}
|
||||||
|
|
||||||
|
startTime := time.Now()
|
||||||
|
timeout := 240
|
||||||
|
for time.Since(startTime) < time.Duration(timeout)*time.Second {
|
||||||
|
if p.CheckLoginStatus() {
|
||||||
|
p.SaveCookies()
|
||||||
|
return true, "login_success"
|
||||||
|
}
|
||||||
|
p.SleepMs(1000)
|
||||||
|
}
|
||||||
|
|
||||||
|
return false, "登录超时,请检查网络或账号状态"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *JianshuPublisher) waitForEditorReady() error {
|
||||||
|
p.LogInfo("等待编辑器加载...")
|
||||||
|
|
||||||
|
plusIcon, err := p.WaitForElementVisible(".fa-plus-circle", 10)
|
||||||
|
if err == nil && plusIcon != nil {
|
||||||
|
p.JSClick(plusIcon)
|
||||||
|
p.LogInfo("已点击新建文章按钮")
|
||||||
|
p.Sleep(2)
|
||||||
|
} else {
|
||||||
|
return fmt.Errorf("未找到新建文章按钮")
|
||||||
|
}
|
||||||
|
|
||||||
|
startTime := time.Now()
|
||||||
|
for time.Since(startTime) < time.Duration(60)*time.Second {
|
||||||
|
editor, _ := p.WaitForElementVisible("#arthur-editor", 2)
|
||||||
|
if editor != nil {
|
||||||
|
p.LogInfo("编辑器加载完成")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
p.SleepMs(1000)
|
||||||
|
}
|
||||||
|
|
||||||
|
p.LogInfo("编辑器加载超时")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *JianshuPublisher) inputTitle() error {
|
||||||
|
p.LogInfo("输入文章标题...")
|
||||||
|
|
||||||
|
today := time.Now().Format("2006-01-02")
|
||||||
|
selector := fmt.Sprintf("input[value='%s']", today)
|
||||||
|
titleInput, err := p.WaitForElementVisible(selector, 5)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("未找到标题输入框")
|
||||||
|
}
|
||||||
|
p.LogInfo("找到标题输入框")
|
||||||
|
p.ClearInput(titleInput)
|
||||||
|
p.SleepMs(300)
|
||||||
|
titleInput.Input("")
|
||||||
|
p.SleepMs(300)
|
||||||
|
titleInput.Input(p.Title)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *JianshuPublisher) inputContent() error {
|
||||||
|
p.LogInfo("输入文章正文...")
|
||||||
|
|
||||||
|
if p.Content == "" {
|
||||||
|
p.LogInfo("内容为空")
|
||||||
|
return fmt.Errorf("内容为空")
|
||||||
|
}
|
||||||
|
|
||||||
|
textArea, err := p.WaitForElementVisible("#arthur-editor", 10)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("未找到文本输入框: %v", err)
|
||||||
|
}
|
||||||
|
p.LogInfo("找到文本输入框")
|
||||||
|
|
||||||
|
p.JSClick(textArea)
|
||||||
|
p.SleepMs(500)
|
||||||
|
|
||||||
|
textArea.Input(p.Content)
|
||||||
|
p.SleepMs(2000)
|
||||||
|
p.LogInfo(fmt.Sprintf("内容已输入,长度: %d", len(p.Content)))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *JianshuPublisher) clickPublish() error {
|
||||||
|
p.LogInfo("点击发布按钮...")
|
||||||
|
|
||||||
|
publishSelectors := []string{
|
||||||
|
"a[data-action='publicize']",
|
||||||
|
".publish-btn",
|
||||||
|
".submit-btn",
|
||||||
|
"button:contains('发布')",
|
||||||
|
".btn-publish",
|
||||||
|
"[class*='publish'] button",
|
||||||
|
"a:contains('发布文章')",
|
||||||
|
}
|
||||||
|
|
||||||
|
var publishBtn *rod.Element
|
||||||
|
for _, selector := range publishSelectors {
|
||||||
|
publishBtn, _ = p.WaitForElementClickable(selector, 5)
|
||||||
|
if publishBtn != nil {
|
||||||
|
visible, _ := publishBtn.Visible()
|
||||||
|
if visible {
|
||||||
|
p.LogInfo(fmt.Sprintf("找到发布按钮: %s", selector))
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if publishBtn == nil {
|
||||||
|
links, _ := p.Page.Elements("a")
|
||||||
|
for _, link := range links {
|
||||||
|
text, _ := link.Text()
|
||||||
|
if strings.Contains(text, "发布文章") || strings.Contains(text, "发布") {
|
||||||
|
publishBtn = link
|
||||||
|
p.LogInfo("通过遍历链接找到发布按钮")
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if publishBtn == nil {
|
||||||
|
return fmt.Errorf("未找到发布按钮")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := publishBtn.Click(proto.InputMouseButtonLeft, 1); err != nil {
|
||||||
|
if err := p.JSClick(publishBtn); err != nil {
|
||||||
|
return fmt.Errorf("点击发布按钮失败: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
p.LogInfo("已点击发布按钮")
|
||||||
|
p.Sleep(3)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *JianshuPublisher) handlePublishDialog() error {
|
||||||
|
p.LogInfo("处理发布弹窗...")
|
||||||
|
|
||||||
|
dialogSelectors := []string{
|
||||||
|
".ant-modal",
|
||||||
|
".el-dialog",
|
||||||
|
".publish-dialog",
|
||||||
|
"[class*='dialog']",
|
||||||
|
"[role='dialog']",
|
||||||
|
}
|
||||||
|
|
||||||
|
var dialog *rod.Element
|
||||||
|
for _, selector := range dialogSelectors {
|
||||||
|
dialog, _ = p.WaitForElementVisible(selector, 5)
|
||||||
|
if dialog != nil {
|
||||||
|
p.LogInfo(fmt.Sprintf("找到发布弹窗: %s", selector))
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if dialog == nil {
|
||||||
|
p.LogInfo("未发现发布弹窗")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
confirmSelectors := []string{
|
||||||
|
"button:contains('确认发布')",
|
||||||
|
"button:contains('确定')",
|
||||||
|
"button:contains('发布')",
|
||||||
|
".confirm-btn",
|
||||||
|
".ok-btn",
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, selector := range confirmSelectors {
|
||||||
|
confirmBtn, err := p.WaitForElementClickable(selector, 3)
|
||||||
|
if err == nil && confirmBtn != nil {
|
||||||
|
p.JSClick(confirmBtn)
|
||||||
|
p.LogInfo("已确认发布")
|
||||||
|
p.Sleep(2)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
closeSelectors := []string{
|
||||||
|
".ant-modal-close",
|
||||||
|
".el-dialog__close",
|
||||||
|
".close-btn",
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, selector := range closeSelectors {
|
||||||
|
closeBtn, err := p.WaitForElementClickable(selector, 2)
|
||||||
|
if err == nil && closeBtn != nil {
|
||||||
|
p.JSClick(closeBtn)
|
||||||
|
p.LogInfo("关闭发布弹窗")
|
||||||
|
p.SleepMs(1000)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *JianshuPublisher) waitForPublishResult(timeout int) (bool, string) {
|
||||||
|
p.LogInfo("等待发布结果...")
|
||||||
|
|
||||||
|
startTime := time.Now()
|
||||||
|
for time.Since(startTime) < time.Duration(timeout)*time.Second {
|
||||||
|
currentURL := p.GetCurrentURL()
|
||||||
|
|
||||||
|
successKeywords := []string{"notes", "articles", "success", "published"}
|
||||||
|
for _, keyword := range successKeywords {
|
||||||
|
if strings.Contains(strings.ToLower(currentURL), keyword) {
|
||||||
|
p.LogInfo(fmt.Sprintf("发布成功!URL: %s", currentURL))
|
||||||
|
return true, "发布成功"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
successSelectors := []string{
|
||||||
|
".ant-message-success",
|
||||||
|
".el-message--success",
|
||||||
|
".toast-success",
|
||||||
|
"[class*='success']",
|
||||||
|
}
|
||||||
|
for _, selector := range successSelectors {
|
||||||
|
elems, _ := p.Page.Elements(selector)
|
||||||
|
for _, elem := range elems {
|
||||||
|
visible, _ := elem.Visible()
|
||||||
|
if visible {
|
||||||
|
text, _ := elem.Text()
|
||||||
|
if strings.Contains(text, "成功") || strings.Contains(strings.ToLower(text), "success") || strings.Contains(text, "已发布") {
|
||||||
|
p.LogInfo(fmt.Sprintf("发布成功: %s", text))
|
||||||
|
return true, text
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
p.SleepMs(1000)
|
||||||
|
}
|
||||||
|
|
||||||
|
return false, "发布结果未知(超时)"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *JianshuPublisher) 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},
|
||||||
|
{"创建编辑器", p.waitForEditorReady},
|
||||||
|
{"输入标题", p.inputTitle},
|
||||||
|
{"导入内容", p.inputContent},
|
||||||
|
{"点击发布", p.clickPublish},
|
||||||
|
}
|
||||||
|
|
||||||
|
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, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
success, message := p.waitForPublishResult(60)
|
||||||
|
if success {
|
||||||
|
p.LogInfo(fmt.Sprintf("🎉 发布成功!%s", message))
|
||||||
|
return true, message
|
||||||
|
}
|
||||||
|
p.LogError(fmt.Sprintf("发布失败: %s", message))
|
||||||
|
return false, message
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,522 @@
|
||||||
|
package publisher
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"geo/internal/config"
|
||||||
|
"log"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/go-rod/rod"
|
||||||
|
"github.com/go-rod/rod/lib/proto"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SohuPublisher struct {
|
||||||
|
*BasePublisher
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSohuPublisher(task *TaskParams, cfg *config.Config, logger *log.Logger) PublisherInerface {
|
||||||
|
return &SohuPublisher{NewBasePublisher(task, cfg, logger)}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *SohuPublisher) CheckLogin() (bool, string) {
|
||||||
|
p.LogInfo("检查登录状态...")
|
||||||
|
|
||||||
|
if err := p.SetupDriver(); err != nil {
|
||||||
|
return false, fmt.Sprintf("浏览器启动失败: %v", err)
|
||||||
|
}
|
||||||
|
defer p.Close()
|
||||||
|
|
||||||
|
p.Page.MustNavigate(p.EditorURL)
|
||||||
|
p.Sleep(3)
|
||||||
|
p.WaitForPageReady(5)
|
||||||
|
|
||||||
|
if p.CheckLoginStatus() {
|
||||||
|
p.SaveCookies()
|
||||||
|
return true, "已登录"
|
||||||
|
}
|
||||||
|
return false, "未登录"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *SohuPublisher) CheckLoginStatus() bool {
|
||||||
|
currentURL := p.GetCurrentURL()
|
||||||
|
if strings.Contains(currentURL, p.LoginURL) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if strings.Contains(currentURL, "clientAuth") {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *SohuPublisher) WaitLogin() (bool, string) {
|
||||||
|
p.LogInfo("开始等待登录...")
|
||||||
|
|
||||||
|
if err := p.SetupDriver(); err != nil {
|
||||||
|
return false, fmt.Sprintf("浏览器启动失败: %v", err)
|
||||||
|
}
|
||||||
|
defer p.Close()
|
||||||
|
|
||||||
|
if p.LoginedURL != "" {
|
||||||
|
p.Page.MustNavigate(p.LoginedURL)
|
||||||
|
p.Sleep(3)
|
||||||
|
|
||||||
|
if p.CheckLoginStatus() {
|
||||||
|
p.SaveCookies()
|
||||||
|
return true, "already_logged_in"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
p.Page.MustNavigate(p.LoginURL)
|
||||||
|
p.LogInfo("请扫描二维码登录...")
|
||||||
|
|
||||||
|
for i := 0; i < 120; i++ {
|
||||||
|
p.SleepMs(1000)
|
||||||
|
if p.CheckLoginStatus() {
|
||||||
|
p.SaveCookies()
|
||||||
|
p.LogInfo("登录成功")
|
||||||
|
return true, "login_success"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false, "登录超时,请检查网络或账号状态"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *SohuPublisher) waitForEditorReady(timeout int) bool {
|
||||||
|
p.LogInfo("等待编辑器加载...")
|
||||||
|
startTime := time.Now()
|
||||||
|
for time.Since(startTime) < time.Duration(timeout)*time.Second {
|
||||||
|
titleSelectors := []string{
|
||||||
|
".publish-title input",
|
||||||
|
"input[placeholder*='标题']",
|
||||||
|
".article-title input",
|
||||||
|
"[class*='title'] input",
|
||||||
|
}
|
||||||
|
for _, selector := range titleSelectors {
|
||||||
|
el, err := p.Page.Element(selector)
|
||||||
|
if err == nil && el != nil {
|
||||||
|
visible, _ := el.Visible()
|
||||||
|
if visible {
|
||||||
|
p.LogInfo("编辑器加载完成")
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
p.SleepMs(1000)
|
||||||
|
}
|
||||||
|
p.LogInfo("编辑器加载超时")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *SohuPublisher) inputTitle() error {
|
||||||
|
p.LogInfo("输入文章标题...")
|
||||||
|
|
||||||
|
titleSelectors := []string{
|
||||||
|
".publish-title input",
|
||||||
|
"input[placeholder*='请输入标题']",
|
||||||
|
"input[placeholder*='标题']",
|
||||||
|
".article-title-input input",
|
||||||
|
".title-input input",
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, selector := range titleSelectors {
|
||||||
|
titleInput, err := p.WaitForElementVisible(selector, 5)
|
||||||
|
if err == nil && titleInput != nil {
|
||||||
|
p.LogInfo(fmt.Sprintf("找到标题输入框: %s", selector))
|
||||||
|
|
||||||
|
if err := p.ClearInput(titleInput); err != nil {
|
||||||
|
titleInput.Input("")
|
||||||
|
}
|
||||||
|
p.SleepMs(300)
|
||||||
|
titleInput.Input(p.Title)
|
||||||
|
p.SleepMs(300)
|
||||||
|
|
||||||
|
p.LogInfo(fmt.Sprintf("标题已输入: %s", p.Title))
|
||||||
|
|
||||||
|
_, err = titleInput.Evaluate(&rod.EvalOptions{
|
||||||
|
JS: `(el) => {
|
||||||
|
el.dispatchEvent(new Event('input', {bubbles: true}));
|
||||||
|
el.dispatchEvent(new Event('change', {bubbles: true}));
|
||||||
|
el.dispatchEvent(new Event('blur', {bubbles: true}));
|
||||||
|
}`,
|
||||||
|
})
|
||||||
|
p.SleepMs(500)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf("未找到标题输入框")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *SohuPublisher) inputContent() error {
|
||||||
|
p.LogInfo("输入文章正文...")
|
||||||
|
|
||||||
|
if p.Content == "" {
|
||||||
|
p.LogInfo("内容为空")
|
||||||
|
return fmt.Errorf("内容为空")
|
||||||
|
}
|
||||||
|
|
||||||
|
editorSelectors := []string{
|
||||||
|
".ql-editor",
|
||||||
|
"[contenteditable='true']",
|
||||||
|
".rich-editor-content",
|
||||||
|
".editor-content",
|
||||||
|
}
|
||||||
|
|
||||||
|
var editor *rod.Element
|
||||||
|
var err error
|
||||||
|
for _, selector := range editorSelectors {
|
||||||
|
editor, err = p.WaitForElementVisible(selector, 10)
|
||||||
|
if err == nil && editor != nil {
|
||||||
|
p.LogInfo(fmt.Sprintf("找到编辑器: %s", selector))
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if editor == nil {
|
||||||
|
return fmt.Errorf("未找到编辑器")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清空编辑器
|
||||||
|
//_, err = editor.Evaluate(&rod.EvalOptions{JS: `el => { el.innerHTML = ''; }`})
|
||||||
|
p.SleepMs(500)
|
||||||
|
|
||||||
|
// 点击编辑器获取焦点
|
||||||
|
p.JSClick(editor)
|
||||||
|
p.SleepMs(500)
|
||||||
|
// 输入内容
|
||||||
|
err = editor.Input(p.Content)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("输入内容失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
p.LogInfo(fmt.Sprintf("内容已输入,长度: %d", len(p.Content)))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *SohuPublisher) setCover() error {
|
||||||
|
p.LogInfo("设置封面...")
|
||||||
|
|
||||||
|
if p.ImagePath == "" {
|
||||||
|
p.LogInfo("未提供封面图片,尝试使用自动封面")
|
||||||
|
return p.setAutoCover()
|
||||||
|
}
|
||||||
|
|
||||||
|
p.LogInfo(fmt.Sprintf("使用封面图片: %s", p.ImagePath))
|
||||||
|
|
||||||
|
coverSelectors := []string{
|
||||||
|
".cover-button .upload-file",
|
||||||
|
".upload-file",
|
||||||
|
"[class*='cover'] .upload-file",
|
||||||
|
".mp-upload",
|
||||||
|
}
|
||||||
|
|
||||||
|
var uploadBtn *rod.Element
|
||||||
|
var err error
|
||||||
|
for _, selector := range coverSelectors {
|
||||||
|
uploadBtn, err = p.WaitForElementClickable(selector, 5)
|
||||||
|
if err == nil && uploadBtn != nil {
|
||||||
|
p.LogInfo(fmt.Sprintf("找到上传封面按钮: %s", selector))
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if uploadBtn == nil {
|
||||||
|
p.LogInfo("未找到上传封面按钮")
|
||||||
|
return p.setAutoCover()
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := uploadBtn.Evaluate(&rod.EvalOptions{
|
||||||
|
JS: `el => el.scrollIntoView({block: 'center'})`,
|
||||||
|
}); err != nil {
|
||||||
|
p.LogInfo(fmt.Sprintf("滚动到按钮失败: %v", err))
|
||||||
|
}
|
||||||
|
p.SleepMs(500)
|
||||||
|
|
||||||
|
fileInputSelectors := []string{
|
||||||
|
"input[type='file'][accept*='image']",
|
||||||
|
".mp-upload input[type='file']",
|
||||||
|
".cover-button input[type='file']",
|
||||||
|
}
|
||||||
|
|
||||||
|
var fileInput *rod.Element
|
||||||
|
for _, selector := range fileInputSelectors {
|
||||||
|
fileInput, err = p.Page.Element(selector)
|
||||||
|
if err == nil && fileInput != nil {
|
||||||
|
p.LogInfo(fmt.Sprintf("找到文件上传输入框: %s", selector))
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if fileInput == nil {
|
||||||
|
p.LogInfo("未找到文件上传输入框")
|
||||||
|
return p.setAutoCover()
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := fileInput.SetFiles([]string{p.ImagePath}); err != nil {
|
||||||
|
p.LogInfo(fmt.Sprintf("上传图片失败: %v", err))
|
||||||
|
return p.setAutoCover()
|
||||||
|
}
|
||||||
|
|
||||||
|
p.LogInfo(fmt.Sprintf("封面图片已上传: %s", p.ImagePath))
|
||||||
|
p.Sleep(3)
|
||||||
|
|
||||||
|
cropConfirm, err := p.WaitForElement(".crop-btn .sure-btn, .confirm-btn", 5)
|
||||||
|
if err == nil && cropConfirm != nil {
|
||||||
|
if err := p.JSClick(cropConfirm); err == nil {
|
||||||
|
p.LogInfo("已确认裁剪")
|
||||||
|
}
|
||||||
|
p.SleepMs(1000)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *SohuPublisher) setAutoCover() error {
|
||||||
|
p.LogInfo("设置为自动封面...")
|
||||||
|
|
||||||
|
autoCoverSelectors := []string{
|
||||||
|
"span:contains('自动')",
|
||||||
|
".cover-title .auto-cover",
|
||||||
|
"[class*='auto']",
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, selector := range autoCoverSelectors {
|
||||||
|
autoElem, err := p.WaitForElement(selector, 3)
|
||||||
|
if err == nil && autoElem != nil {
|
||||||
|
if err := p.JSClick(autoElem); err == nil {
|
||||||
|
p.LogInfo("已选择自动封面")
|
||||||
|
p.SleepMs(500)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
p.LogInfo("未找到自动封面选项,跳过封面设置")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *SohuPublisher) setAbstract() error {
|
||||||
|
p.LogInfo("设置摘要...")
|
||||||
|
|
||||||
|
abstractSelectors := []string{
|
||||||
|
".abstract-main textarea",
|
||||||
|
"textarea[placeholder*='摘要']",
|
||||||
|
".abstract textarea",
|
||||||
|
}
|
||||||
|
|
||||||
|
var abstractInput *rod.Element
|
||||||
|
var err error
|
||||||
|
for _, selector := range abstractSelectors {
|
||||||
|
abstractInput, err = p.WaitForElementVisible(selector, 3)
|
||||||
|
if err == nil && abstractInput != nil {
|
||||||
|
p.LogInfo(fmt.Sprintf("找到摘要输入框: %s", selector))
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if abstractInput != nil {
|
||||||
|
if err := abstractInput.SelectAllText(); err == nil {
|
||||||
|
abstractInput.Input("")
|
||||||
|
}
|
||||||
|
p.SleepMs(300)
|
||||||
|
|
||||||
|
abstractText := p.Content
|
||||||
|
if len(abstractText) > 120 {
|
||||||
|
abstractText = abstractText[:120]
|
||||||
|
}
|
||||||
|
abstractInput.Input(abstractText)
|
||||||
|
p.LogInfo("摘要已设置")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
p.LogInfo("未找到摘要输入框,跳过")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *SohuPublisher) setTags() error {
|
||||||
|
if len(p.Tags) == 0 {
|
||||||
|
p.LogInfo("无标签需要设置")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
p.LogInfo(fmt.Sprintf("设置标签: %v", p.Tags))
|
||||||
|
|
||||||
|
tagInputSelectors := []string{
|
||||||
|
"input[placeholder*='标签']",
|
||||||
|
".tag-input input",
|
||||||
|
"[class*='tag'] input",
|
||||||
|
}
|
||||||
|
|
||||||
|
var tagInput *rod.Element
|
||||||
|
var err error
|
||||||
|
for _, selector := range tagInputSelectors {
|
||||||
|
tagInput, err = p.WaitForElement(selector, 3)
|
||||||
|
if err == nil && tagInput != nil {
|
||||||
|
p.LogInfo(fmt.Sprintf("找到标签输入框: %s", selector))
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if tagInput != nil {
|
||||||
|
for _, tag := range p.Tags {
|
||||||
|
tagInput.Input(tag)
|
||||||
|
p.SleepMs(300)
|
||||||
|
tagInput.Input("\n")
|
||||||
|
p.SleepMs(300)
|
||||||
|
}
|
||||||
|
p.LogInfo("标签设置成功")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
p.LogInfo("未找到标签输入框(可能无需设置)")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *SohuPublisher) clickPublish() error {
|
||||||
|
p.LogInfo("点击发布按钮...")
|
||||||
|
|
||||||
|
publishSelectors := []string{
|
||||||
|
".publish-report-btn.active",
|
||||||
|
"button:contains('发布')",
|
||||||
|
".publish-btn",
|
||||||
|
".submit-btn",
|
||||||
|
"[class*='publish'] button",
|
||||||
|
}
|
||||||
|
|
||||||
|
var publishBtn *rod.Element
|
||||||
|
var err error
|
||||||
|
for _, selector := range publishSelectors {
|
||||||
|
publishBtn, err = p.WaitForElementClickable(selector, 5)
|
||||||
|
if err == nil && publishBtn != nil {
|
||||||
|
visible, _ := publishBtn.Visible()
|
||||||
|
if visible {
|
||||||
|
p.LogInfo(fmt.Sprintf("找到发布按钮: %s", selector))
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if publishBtn == nil {
|
||||||
|
buttons, err := p.Page.Elements("button")
|
||||||
|
if err == nil {
|
||||||
|
for _, btn := range buttons {
|
||||||
|
text, _ := btn.Text()
|
||||||
|
if text == "发布" || strings.Contains(text, "发布") {
|
||||||
|
publishBtn = btn
|
||||||
|
p.LogInfo("通过遍历按钮找到发布按钮")
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if publishBtn == nil {
|
||||||
|
return fmt.Errorf("未找到发布按钮")
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := publishBtn.Evaluate(&rod.EvalOptions{
|
||||||
|
JS: `el => el.scrollIntoView({block: 'center', behavior: 'smooth'})`,
|
||||||
|
}); err != nil {
|
||||||
|
p.LogInfo(fmt.Sprintf("滚动到按钮失败: %v", err))
|
||||||
|
}
|
||||||
|
p.SleepMs(500)
|
||||||
|
|
||||||
|
if err := publishBtn.Click(proto.InputMouseButtonLeft, 1); err != nil {
|
||||||
|
if err := p.JSClick(publishBtn); err != nil {
|
||||||
|
return fmt.Errorf("点击发布按钮失败: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
p.LogInfo("已点击发布按钮")
|
||||||
|
p.Sleep(3)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *SohuPublisher) waitForPublishResult(timeout int) (bool, string) {
|
||||||
|
p.LogInfo("等待发布结果...")
|
||||||
|
|
||||||
|
startTime := time.Now()
|
||||||
|
for time.Since(startTime) < time.Duration(timeout)*time.Second {
|
||||||
|
currentURL := p.GetCurrentURL()
|
||||||
|
|
||||||
|
if strings.Contains(currentURL, "contentManagement") {
|
||||||
|
p.LogInfo(fmt.Sprintf("发布成功!URL: %s", currentURL))
|
||||||
|
return true, "发布成功"
|
||||||
|
}
|
||||||
|
|
||||||
|
successMsgs, _ := p.Page.Elements(".el-message--success, .toast-success, .message-success, [class*='success']")
|
||||||
|
for _, elem := range successMsgs {
|
||||||
|
visible, _ := elem.Visible()
|
||||||
|
if visible {
|
||||||
|
text, _ := elem.Text()
|
||||||
|
if strings.Contains(text, "成功") || strings.Contains(strings.ToLower(text), "success") || strings.Contains(text, "已发布") {
|
||||||
|
p.LogInfo(fmt.Sprintf("发布成功: %s", text))
|
||||||
|
return true, text
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
p.SleepMs(1000)
|
||||||
|
}
|
||||||
|
|
||||||
|
return false, "发布结果未知(超时)"
|
||||||
|
}
|
||||||
|
|
||||||
|
// InitPage 初始化页面
|
||||||
|
func (p *SohuPublisher) InitPage() error {
|
||||||
|
|
||||||
|
// 尝试加载cookies并检查登录状态
|
||||||
|
if err := p.LoadCookies(); err == nil {
|
||||||
|
p.Page.MustNavigate(p.EditorURL)
|
||||||
|
p.WaitForPageReady(5)
|
||||||
|
p.Sleep(2)
|
||||||
|
}
|
||||||
|
// 统一检查登录状态
|
||||||
|
if !p.CheckLoginStatus() {
|
||||||
|
p.LogInfo("未登录或登录已过期,需要重新登录")
|
||||||
|
return fmt.Errorf("需要登录")
|
||||||
|
}
|
||||||
|
p.SaveCookies()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *SohuPublisher) 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},
|
||||||
|
{"输入标题", p.inputTitle},
|
||||||
|
{"导入内容", p.inputContent},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, step := range steps {
|
||||||
|
if err := step.fn(); err != nil {
|
||||||
|
p.LogStep(step.name, false, err.Error())
|
||||||
|
return false, fmt.Sprintf("%s失败: %s", step.name, err.Error())
|
||||||
|
}
|
||||||
|
p.LogStep(step.name, true, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := p.clickPublish(); err != nil {
|
||||||
|
p.LogStep("点击发布", false, err.Error())
|
||||||
|
return false, err.Error()
|
||||||
|
}
|
||||||
|
p.LogStep("点击发布", true, "")
|
||||||
|
|
||||||
|
success, message := p.waitForPublishResult(60)
|
||||||
|
if success {
|
||||||
|
p.LogInfo(fmt.Sprintf("🎉 发布成功!%s", message))
|
||||||
|
return true, message
|
||||||
|
}
|
||||||
|
p.LogError(fmt.Sprintf("发布失败: %s", message))
|
||||||
|
return false, message
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,582 @@
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
@ -317,75 +317,6 @@ func (p *ToutiaoPublisher) inputContent() error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetContentEditableHTML 设置 contenteditable 元素的 HTML 内容
|
|
||||||
func (p *ToutiaoPublisher) SetContentEditableHTML(element *rod.Element, html string) error {
|
|
||||||
_, err := element.Evaluate(&rod.EvalOptions{
|
|
||||||
JS: `(el, val) => { el.innerHTML = val; el.dispatchEvent(new Event('input', {bubbles: true})); }`,
|
|
||||||
JSArgs: []interface{}{html},
|
|
||||||
})
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// findFileInput 查找文件上传输入框
|
|
||||||
func (p *ToutiaoPublisher) findFileInput() (*rod.Element, error) {
|
|
||||||
selectors := []string{
|
|
||||||
"input[type='file']",
|
|
||||||
".byte-drawer-inner input[type='file']",
|
|
||||||
"input[accept*='image']",
|
|
||||||
".upload-input input[type='file']",
|
|
||||||
"button input[type='file']",
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, selector := range selectors {
|
|
||||||
el, err := p.Page.Element(selector)
|
|
||||||
if err == nil && el != nil {
|
|
||||||
p.LogInfo(fmt.Sprintf("找到文件上传输入框: %s", selector))
|
|
||||||
return el, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil, fmt.Errorf("未找到文件上传输入框")
|
|
||||||
}
|
|
||||||
|
|
||||||
// clickConfirmButton 点击确认按钮(带重试机制)
|
|
||||||
func (p *ToutiaoPublisher) clickConfirmButton() error {
|
|
||||||
for attempt := 1; attempt <= 10; attempt++ {
|
|
||||||
p.LogInfo(fmt.Sprintf("第 %d 次尝试点击确认按钮", attempt))
|
|
||||||
|
|
||||||
// 查找确认按钮
|
|
||||||
confirmBtn, err := p.Page.Element("button[data-e2e='imageUploadConfirm-btn']")
|
|
||||||
if err == nil && confirmBtn != nil {
|
|
||||||
// 检查按钮是否可用
|
|
||||||
if err := p.JSClick(confirmBtn); err != nil {
|
|
||||||
p.LogInfo(fmt.Sprintf("点击确认按钮失败: %v", err))
|
|
||||||
} else {
|
|
||||||
p.LogInfo("已点击确认按钮")
|
|
||||||
p.SleepMs(1000)
|
|
||||||
|
|
||||||
// 检查弹窗是否已关闭
|
|
||||||
if p.isDrawerClosed() {
|
|
||||||
p.LogInfo("弹窗已成功关闭,封面设置完成")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
p.LogInfo(fmt.Sprintf("第 %d 次尝试:未找到确认按钮", attempt))
|
|
||||||
}
|
|
||||||
|
|
||||||
if attempt < 10 {
|
|
||||||
time.Sleep(2 * time.Second)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 最终检查弹窗状态
|
|
||||||
if p.isDrawerClosed() {
|
|
||||||
p.LogInfo("弹窗已关闭,封面设置成功")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return fmt.Errorf("确认按钮点击后弹窗未关闭")
|
|
||||||
}
|
|
||||||
|
|
||||||
// isDrawerClosed 检查弹窗是否已关闭
|
// isDrawerClosed 检查弹窗是否已关闭
|
||||||
func (p *ToutiaoPublisher) isDrawerClosed() bool {
|
func (p *ToutiaoPublisher) isDrawerClosed() bool {
|
||||||
drawerWrappers, err := p.Page.Elements(".byte-drawer-wrapper")
|
drawerWrappers, err := p.Page.Elements(".byte-drawer-wrapper")
|
||||||
|
|
@ -462,7 +393,7 @@ func (p *ToutiaoPublisher) clickPublish() error {
|
||||||
func (p *ToutiaoPublisher) waitForPublishResult() (bool, string) {
|
func (p *ToutiaoPublisher) waitForPublishResult() (bool, string) {
|
||||||
p.LogInfo("等待发布结果...")
|
p.LogInfo("等待发布结果...")
|
||||||
|
|
||||||
for attempt := 0; attempt < 60; attempt++ {
|
for attempt := 0; attempt < p.MaxRetries; attempt++ {
|
||||||
currentURL := p.GetCurrentURL()
|
currentURL := p.GetCurrentURL()
|
||||||
p.LogInfo(fmt.Sprintf("第 %d 次检查 - URL: %s", attempt+1, currentURL))
|
p.LogInfo(fmt.Sprintf("第 %d 次检查 - URL: %s", attempt+1, currentURL))
|
||||||
|
|
||||||
|
|
@ -509,7 +440,7 @@ func (p *ToutiaoPublisher) waitForPublishResult() (bool, string) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
p.SleepMs(1000)
|
p.SleepMs(p.RetryDelay)
|
||||||
}
|
}
|
||||||
|
|
||||||
p.LogWarning("发布结果未知")
|
p.LogWarning("发布结果未知")
|
||||||
|
|
@ -519,14 +450,11 @@ func (p *ToutiaoPublisher) waitForPublishResult() (bool, string) {
|
||||||
// InitPage 初始化页面
|
// InitPage 初始化页面
|
||||||
func (p *ToutiaoPublisher) InitPage() error {
|
func (p *ToutiaoPublisher) InitPage() error {
|
||||||
// 访问发布页面
|
// 访问发布页面
|
||||||
p.Page.MustNavigate(p.EditorURL)
|
|
||||||
p.Sleep(2)
|
|
||||||
p.WaitForPageReady(5)
|
|
||||||
|
|
||||||
// 尝试加载cookies并检查登录状态
|
// 尝试加载cookies并检查登录状态
|
||||||
if err := p.LoadCookies(); err == nil {
|
if err := p.LoadCookies(); err == nil {
|
||||||
p.RefreshPage()
|
p.Page.MustNavigate(p.EditorURL)
|
||||||
p.Sleep(2)
|
p.WaitForPageReady(5)
|
||||||
if p.CheckLoginStatus() {
|
if p.CheckLoginStatus() {
|
||||||
p.SaveCookies()
|
p.SaveCookies()
|
||||||
return nil
|
return nil
|
||||||
|
|
@ -553,7 +481,6 @@ func (p *ToutiaoPublisher) PublishNote() (bool, string) {
|
||||||
}{
|
}{
|
||||||
{"初始化页面", p.InitPage},
|
{"初始化页面", p.InitPage},
|
||||||
{"输入内容", p.inputContent},
|
{"输入内容", p.inputContent},
|
||||||
//{"上传封面", p.uploadCover},
|
|
||||||
{"输入标题", p.inputTitle},
|
{"输入标题", p.inputTitle},
|
||||||
{"点击发布", p.clickPublish},
|
{"点击发布", p.clickPublish},
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,621 @@
|
||||||
|
package publisher
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"geo/internal/config"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/go-rod/rod"
|
||||||
|
"github.com/go-rod/rod/lib/proto"
|
||||||
|
)
|
||||||
|
|
||||||
|
type WangyiPublisher struct {
|
||||||
|
*BasePublisher
|
||||||
|
Category string
|
||||||
|
IsOriginal bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewWangyiPublisher 构造函数
|
||||||
|
func NewWangyiPublisher(task *TaskParams, cfg *config.Config, logger *log.Logger) PublisherInerface {
|
||||||
|
return &WangyiPublisher{
|
||||||
|
BasePublisher: NewBasePublisher(task, cfg, logger),
|
||||||
|
Category: "",
|
||||||
|
IsOriginal: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *WangyiPublisher) CheckLogin() (bool, string) {
|
||||||
|
p.LogInfo("检查登录状态...")
|
||||||
|
|
||||||
|
if err := p.SetupDriver(); err != nil {
|
||||||
|
return false, fmt.Sprintf("浏览器启动失败: %v", err)
|
||||||
|
}
|
||||||
|
defer p.Close()
|
||||||
|
|
||||||
|
p.Page.MustNavigate(p.EditorURL)
|
||||||
|
p.Sleep(3)
|
||||||
|
p.WaitForPageReady(5)
|
||||||
|
|
||||||
|
if p.CheckLoginStatus() {
|
||||||
|
p.SaveCookies()
|
||||||
|
return true, "已登录"
|
||||||
|
}
|
||||||
|
return false, "未登录"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *WangyiPublisher) CheckLoginStatus() bool {
|
||||||
|
currentURL := p.GetCurrentURL()
|
||||||
|
|
||||||
|
// 如果在登录页面,未登录
|
||||||
|
if strings.Contains(currentURL, p.LoginURL) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *WangyiPublisher) WaitLogin() (bool, string) {
|
||||||
|
p.LogInfo("开始等待登录...")
|
||||||
|
|
||||||
|
if err := p.SetupDriver(); err != nil {
|
||||||
|
return false, fmt.Sprintf("浏览器启动失败: %v", err)
|
||||||
|
}
|
||||||
|
defer p.Close()
|
||||||
|
|
||||||
|
// 先尝试访问已登录页面
|
||||||
|
if p.LoginedURL != "" {
|
||||||
|
p.Page.MustNavigate(p.LoginedURL)
|
||||||
|
p.Sleep(3)
|
||||||
|
|
||||||
|
if p.CheckLoginStatus() {
|
||||||
|
p.SaveCookies()
|
||||||
|
p.LogInfo("已有登录状态")
|
||||||
|
return true, "already_logged_in"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 未登录,跳转到登录页
|
||||||
|
if p.LoginURL != "" {
|
||||||
|
p.Page.MustNavigate(p.LoginURL)
|
||||||
|
p.WaitForPageReady(5)
|
||||||
|
}
|
||||||
|
|
||||||
|
p.LogInfo("请扫码登录...")
|
||||||
|
|
||||||
|
// 等待登录完成,最多120秒
|
||||||
|
for i := 0; i < 120; i++ {
|
||||||
|
currentURL := p.GetCurrentURL()
|
||||||
|
|
||||||
|
// 检查是否跳转到发布页面或主页
|
||||||
|
if p.EditorURL != "" && strings.Contains(currentURL, p.EditorURL) {
|
||||||
|
p.SaveCookies()
|
||||||
|
p.LogInfo("登录成功")
|
||||||
|
return true, "login_success"
|
||||||
|
}
|
||||||
|
if p.LoginedURL != "" && strings.Contains(currentURL, p.LoginedURL) {
|
||||||
|
p.SaveCookies()
|
||||||
|
p.LogInfo("登录成功")
|
||||||
|
return true, "login_success"
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查登录状态
|
||||||
|
if p.CheckLoginStatus() {
|
||||||
|
p.SaveCookies()
|
||||||
|
p.LogInfo("登录成功")
|
||||||
|
return true, "login_success"
|
||||||
|
}
|
||||||
|
|
||||||
|
time.Sleep(1 * time.Second)
|
||||||
|
}
|
||||||
|
|
||||||
|
return false, "登录超时,请检查网络或账号状态"
|
||||||
|
}
|
||||||
|
|
||||||
|
// inputTitle 输入标题
|
||||||
|
func (p *WangyiPublisher) inputTitle() error {
|
||||||
|
p.LogInfo("输入文章标题...")
|
||||||
|
|
||||||
|
titleSelectors := []string{
|
||||||
|
"textarea[placeholder*='标题']",
|
||||||
|
"input[placeholder*='标题']",
|
||||||
|
}
|
||||||
|
|
||||||
|
var titleInput *rod.Element
|
||||||
|
var err error
|
||||||
|
|
||||||
|
for _, selector := range titleSelectors {
|
||||||
|
titleInput, err = p.WaitForElementVisible(selector, 5)
|
||||||
|
if err == nil && titleInput != nil {
|
||||||
|
p.LogInfo(fmt.Sprintf("找到标题输入框: %s", selector))
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if titleInput == nil {
|
||||||
|
return fmt.Errorf("未找到标题输入框")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 点击获取焦点
|
||||||
|
if err := titleInput.Click(proto.InputMouseButtonLeft, 1); err != nil {
|
||||||
|
return fmt.Errorf("点击标题框失败: %v", err)
|
||||||
|
}
|
||||||
|
p.SleepMs(300)
|
||||||
|
|
||||||
|
// 清空输入框
|
||||||
|
if err := p.ClearInput(titleInput); err != nil {
|
||||||
|
titleInput.Input("")
|
||||||
|
}
|
||||||
|
p.SleepMs(300)
|
||||||
|
|
||||||
|
// 输入标题
|
||||||
|
if err := p.SetInputValue(titleInput, p.Title); err != nil {
|
||||||
|
titleInput.Input(p.Title)
|
||||||
|
}
|
||||||
|
|
||||||
|
p.LogInfo(fmt.Sprintf("标题已输入: %s", p.Title))
|
||||||
|
p.SleepMs(500)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// insertImageToWord 将图片插入到Word文档头部
|
||||||
|
func (p *WangyiPublisher) insertImageToWord(docPath, imagePath string) (string, error) {
|
||||||
|
// 由于Go中处理Word文档需要额外库,这里先返回原路径
|
||||||
|
// 如果需要此功能,可以使用 unioffice 或 excelize 等库
|
||||||
|
p.LogInfo(fmt.Sprintf("插入图片到Word: %s -> %s", imagePath, docPath))
|
||||||
|
return docPath, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// importDocument 导入Word文档
|
||||||
|
func (p *WangyiPublisher) importDocument() error {
|
||||||
|
if p.SourcePath == "" {
|
||||||
|
p.LogInfo("未提供文档路径,使用内容文本")
|
||||||
|
return p.inputContentDirect()
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := os.Stat(p.SourcePath); os.IsNotExist(err) {
|
||||||
|
p.LogWarning(fmt.Sprintf("文档不存在: %s", p.SourcePath))
|
||||||
|
return p.inputContentDirect()
|
||||||
|
}
|
||||||
|
|
||||||
|
p.LogInfo(fmt.Sprintf("尝试导入文档: %s", p.SourcePath))
|
||||||
|
|
||||||
|
// 查找导入文档按钮
|
||||||
|
importSelectors := []string{
|
||||||
|
".ne-rich-editor-upload-button",
|
||||||
|
"[class*='rich-editor-upload']",
|
||||||
|
"label[for='ne-rich-editor-upload-input']",
|
||||||
|
".rich-editor-panel-item",
|
||||||
|
}
|
||||||
|
|
||||||
|
var importBtn *rod.Element
|
||||||
|
var err error
|
||||||
|
|
||||||
|
for _, selector := range importSelectors {
|
||||||
|
importBtn, err = p.WaitForElementClickable(selector, 5)
|
||||||
|
if err == nil && importBtn != nil {
|
||||||
|
p.LogInfo(fmt.Sprintf("找到导入文档按钮: %s", selector))
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if importBtn == nil {
|
||||||
|
// 尝试通过文本查找
|
||||||
|
importBtn, err = p.Page.Element("button:contains('导入文档')")
|
||||||
|
if err != nil {
|
||||||
|
p.LogWarning("未找到导入文档按钮,使用直接输入内容")
|
||||||
|
return p.inputContentDirect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
p.SleepMs(500)
|
||||||
|
|
||||||
|
// 查找文件上传输入框
|
||||||
|
fileInputSelectors := []string{
|
||||||
|
"#ne-rich-editor-upload-input",
|
||||||
|
"input[type='file'][accept*='.doc']",
|
||||||
|
"input[type='file'][accept*='.docx']",
|
||||||
|
".ne-rich-editor-upload-button input[type='file']",
|
||||||
|
}
|
||||||
|
|
||||||
|
var fileInput *rod.Element
|
||||||
|
for _, selector := range fileInputSelectors {
|
||||||
|
fileInput, err = p.Page.Element(selector)
|
||||||
|
if err == nil && fileInput != nil {
|
||||||
|
p.LogInfo(fmt.Sprintf("找到文件上传输入框: %s", selector))
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if fileInput == nil {
|
||||||
|
p.LogWarning("未找到文件上传输入框,使用直接输入内容")
|
||||||
|
return p.inputContentDirect()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 上传文件
|
||||||
|
if err := fileInput.SetFiles([]string{p.SourcePath}); err != nil {
|
||||||
|
p.LogWarning(fmt.Sprintf("上传文件失败: %v,使用直接输入内容", err))
|
||||||
|
return p.inputContentDirect()
|
||||||
|
}
|
||||||
|
p.LogInfo(fmt.Sprintf("文档已上传: %s", p.SourcePath))
|
||||||
|
p.SleepMs(3000)
|
||||||
|
|
||||||
|
// 等待导入完成
|
||||||
|
for attempt := 0; attempt < 30; attempt++ {
|
||||||
|
// 检查内容编辑器是否有内容
|
||||||
|
contentEditor, err := p.Page.Element(".rich-editor-stage, .ProseMirror, [contenteditable='true']")
|
||||||
|
if err == nil && contentEditor != nil {
|
||||||
|
text, _ := contentEditor.Text()
|
||||||
|
if len(text) > 0 {
|
||||||
|
p.LogInfo("文档导入成功")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
p.SleepMs(1000)
|
||||||
|
}
|
||||||
|
|
||||||
|
p.LogInfo("文档导入完成")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// inputContentDirect 直接输入内容
|
||||||
|
func (p *WangyiPublisher) inputContentDirect() error {
|
||||||
|
p.LogInfo("直接输入文章内容...")
|
||||||
|
|
||||||
|
// 查找内容编辑器
|
||||||
|
contentSelectors := []string{
|
||||||
|
".rich-editor-stage",
|
||||||
|
".ProseMirror",
|
||||||
|
"[contenteditable='true']",
|
||||||
|
}
|
||||||
|
|
||||||
|
var contentEditor *rod.Element
|
||||||
|
var err error
|
||||||
|
|
||||||
|
for _, selector := range contentSelectors {
|
||||||
|
contentEditor, err = p.WaitForElementVisible(selector, 10)
|
||||||
|
if err == nil && contentEditor != nil {
|
||||||
|
p.LogInfo(fmt.Sprintf("找到内容编辑器: %s", selector))
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if contentEditor == nil {
|
||||||
|
return fmt.Errorf("未找到内容编辑器")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 点击获取焦点
|
||||||
|
if err := contentEditor.Click(proto.InputMouseButtonLeft, 1); err != nil {
|
||||||
|
return fmt.Errorf("点击编辑器失败: %v", err)
|
||||||
|
}
|
||||||
|
p.SleepMs(500)
|
||||||
|
|
||||||
|
// 清空现有内容
|
||||||
|
if err := p.ClearContentEditable(contentEditor); err != nil {
|
||||||
|
p.LogInfo(fmt.Sprintf("清空编辑器失败: %v", err))
|
||||||
|
}
|
||||||
|
p.SleepMs(300)
|
||||||
|
|
||||||
|
// 输入内容
|
||||||
|
if err := p.SetContentEditable(contentEditor, p.Content); err != nil {
|
||||||
|
contentEditor.Input(p.Content)
|
||||||
|
}
|
||||||
|
|
||||||
|
p.LogInfo(fmt.Sprintf("内容已输入,长度: %d", len(p.Content)))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// setCover 设置封面为自动模式
|
||||||
|
func (p *WangyiPublisher) setCover() error {
|
||||||
|
p.LogInfo("设置封面为自动模式...")
|
||||||
|
|
||||||
|
// 先找到 class="post-footer__container" 的 div,点击
|
||||||
|
footerDiv, err := p.Page.ElementX("//span[contains(text(), '设置区')]")
|
||||||
|
if err != nil {
|
||||||
|
p.LogWarning("未找到 设置区")
|
||||||
|
return fmt.Errorf("未找到设置区")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 点击这个div
|
||||||
|
if err := p.JSClick(footerDiv); err != nil {
|
||||||
|
p.LogInfo(fmt.Sprintf("点击 设置区 失败: %v", err))
|
||||||
|
}
|
||||||
|
p.SleepMs(2000)
|
||||||
|
|
||||||
|
// 查找包含"自动"文本的span
|
||||||
|
autoSpan, err := p.Page.ElementX("//span[contains(text(), '自动')]")
|
||||||
|
if err != nil {
|
||||||
|
p.LogWarning("未找到包含'自动'文本的span")
|
||||||
|
return fmt.Errorf("未找到包含'自动'文本的span")
|
||||||
|
}
|
||||||
|
|
||||||
|
p.SleepMs(500)
|
||||||
|
//
|
||||||
|
// 查找父级label并点击
|
||||||
|
parentLabel, err := autoSpan.Parent()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("未找到父级label元素: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := p.JSClick(parentLabel); err != nil {
|
||||||
|
return fmt.Errorf("点击自动封面选项失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
p.LogInfo("已点击自动封面选项")
|
||||||
|
p.SleepMs(1000)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// clickPublish 点击发布按钮
|
||||||
|
func (p *WangyiPublisher) clickPublish() error {
|
||||||
|
p.LogInfo("点击发布按钮...")
|
||||||
|
|
||||||
|
publishSelectors := []string{
|
||||||
|
"button:contains('发布')",
|
||||||
|
"button.ne-button:contains('发布')",
|
||||||
|
"button.primary_button",
|
||||||
|
"button.ne-button-color-primary",
|
||||||
|
".netease-button.primary_button",
|
||||||
|
".publish-btn",
|
||||||
|
".submit-btn",
|
||||||
|
}
|
||||||
|
|
||||||
|
var publishBtn *rod.Element
|
||||||
|
var err error
|
||||||
|
|
||||||
|
for _, selector := range publishSelectors {
|
||||||
|
publishBtn, err = p.WaitForElementClickable(selector, 3)
|
||||||
|
if err == nil && publishBtn != nil {
|
||||||
|
p.LogInfo(fmt.Sprintf("找到发布按钮: %s", selector))
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if publishBtn == nil {
|
||||||
|
// 遍历所有按钮查找文本为"发布"的
|
||||||
|
buttons, err := p.Page.Elements("button")
|
||||||
|
if err == nil {
|
||||||
|
for _, btn := range buttons {
|
||||||
|
text, _ := btn.Text()
|
||||||
|
if text == "发布" || strings.Contains(text, "发布") {
|
||||||
|
publishBtn = btn
|
||||||
|
p.LogInfo("通过遍历按钮找到发布按钮")
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if publishBtn == nil {
|
||||||
|
return fmt.Errorf("未找到发布按钮")
|
||||||
|
}
|
||||||
|
|
||||||
|
p.SleepMs(500)
|
||||||
|
|
||||||
|
// 点击发布按钮
|
||||||
|
if err := p.JSClick(publishBtn); err != nil {
|
||||||
|
return fmt.Errorf("点击发布按钮失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
p.LogInfo("已点击发布按钮")
|
||||||
|
p.SleepMs(3000)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// waitForPublishResult 等待发布结果
|
||||||
|
func (p *WangyiPublisher) waitForPublishResult() (bool, string) {
|
||||||
|
p.LogInfo("等待发布结果...")
|
||||||
|
|
||||||
|
timeout := 60
|
||||||
|
retryInterval := 5
|
||||||
|
maxRetries := 5
|
||||||
|
lastClickTime := int64(0)
|
||||||
|
retryCount := 0
|
||||||
|
|
||||||
|
startTime := time.Now()
|
||||||
|
errorText := ""
|
||||||
|
|
||||||
|
for time.Since(startTime).Seconds() < float64(timeout) {
|
||||||
|
currentURL := p.GetCurrentURL()
|
||||||
|
|
||||||
|
// 检查是否发布成功
|
||||||
|
successKeywords := []string{"success", "article-manage", "content-manage", "list", "article/list"}
|
||||||
|
for _, keyword := range successKeywords {
|
||||||
|
if strings.Contains(currentURL, keyword) {
|
||||||
|
p.LogInfo(fmt.Sprintf("发布成功!URL: %s", currentURL))
|
||||||
|
return true, "发布成功"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查成功提示
|
||||||
|
successSelectors := []string{
|
||||||
|
"[class*='success']",
|
||||||
|
".toast-success",
|
||||||
|
".message-success",
|
||||||
|
".ne-message-success",
|
||||||
|
}
|
||||||
|
for _, selector := range successSelectors {
|
||||||
|
msgs, err := p.Page.Elements(selector)
|
||||||
|
if err == nil {
|
||||||
|
for _, elem := range msgs {
|
||||||
|
visible, _ := elem.Visible()
|
||||||
|
if visible {
|
||||||
|
text, _ := elem.Text()
|
||||||
|
if text != "" && (strings.Contains(text, "成功") || strings.Contains(text, "已发布")) {
|
||||||
|
p.LogInfo(fmt.Sprintf("发布成功: %s", text))
|
||||||
|
return true, text
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查错误提示
|
||||||
|
errorFound := false
|
||||||
|
errorSelectors := []string{
|
||||||
|
"[class*='error']",
|
||||||
|
".toast-error",
|
||||||
|
".message-error",
|
||||||
|
".ne-message-error",
|
||||||
|
}
|
||||||
|
for _, selector := range errorSelectors {
|
||||||
|
msgs, err := p.Page.Elements(selector)
|
||||||
|
if err == nil {
|
||||||
|
for _, elem := range msgs {
|
||||||
|
visible, _ := elem.Visible()
|
||||||
|
if visible {
|
||||||
|
text, _ := elem.Text()
|
||||||
|
if text != "" && (strings.Contains(text, "失败") || strings.Contains(text, "error")) {
|
||||||
|
errorText = text
|
||||||
|
errorFound = true
|
||||||
|
p.LogError(fmt.Sprintf("发布失败: %s", errorText))
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if errorFound {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否需要重试
|
||||||
|
currentTime := time.Now().Unix()
|
||||||
|
if currentTime-lastClickTime >= int64(retryInterval) && retryCount < maxRetries {
|
||||||
|
if strings.Contains(currentURL, p.EditorURL) {
|
||||||
|
p.LogInfo(fmt.Sprintf("第 %d 次重试,重新点击发布按钮...", retryCount+1))
|
||||||
|
|
||||||
|
// 如果有错误提示,先尝试关闭
|
||||||
|
if errorFound {
|
||||||
|
closeBtns, _ := p.Page.Elements(".close-btn, .ne-message-close, [class*='close']")
|
||||||
|
for _, btn := range closeBtns {
|
||||||
|
visible, _ := btn.Visible()
|
||||||
|
if visible {
|
||||||
|
p.JSClick(btn)
|
||||||
|
p.LogInfo("关闭错误提示")
|
||||||
|
p.SleepMs(1000)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重新点击发布按钮
|
||||||
|
if err := p.clickPublish(); err == nil {
|
||||||
|
retryCount++
|
||||||
|
lastClickTime = currentTime
|
||||||
|
errorFound = false
|
||||||
|
p.LogInfo(fmt.Sprintf("第 %d 次重试已点击发布按钮,继续等待结果...", retryCount))
|
||||||
|
} else {
|
||||||
|
p.LogWarning(fmt.Sprintf("第 %d 次重试点击发布按钮失败", retryCount+1))
|
||||||
|
lastClickTime = currentTime
|
||||||
|
retryCount++
|
||||||
|
}
|
||||||
|
|
||||||
|
p.SleepMs(2000)
|
||||||
|
continue
|
||||||
|
} else if errorFound {
|
||||||
|
return false, fmt.Sprintf("发布失败: %s", errorText)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
p.SleepMs(1000)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 超时后最后一次尝试
|
||||||
|
if retryCount < maxRetries {
|
||||||
|
p.LogInfo("超时前最后一次尝试点击发布按钮...")
|
||||||
|
p.clickPublish()
|
||||||
|
p.SleepMs(5000)
|
||||||
|
|
||||||
|
// 再次检查结果
|
||||||
|
successSelectors := []string{
|
||||||
|
"[class*='success']",
|
||||||
|
".toast-success",
|
||||||
|
".message-success",
|
||||||
|
}
|
||||||
|
for _, selector := range successSelectors {
|
||||||
|
msgs, err := p.Page.Elements(selector)
|
||||||
|
if err == nil {
|
||||||
|
for _, elem := range msgs {
|
||||||
|
text, _ := elem.Text()
|
||||||
|
if text != "" && strings.Contains(text, "成功") {
|
||||||
|
return true, text
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false, "发布结果未知(超时)"
|
||||||
|
}
|
||||||
|
|
||||||
|
// InitPage 初始化页面
|
||||||
|
func (p *WangyiPublisher) InitPage() error {
|
||||||
|
|
||||||
|
// 尝试加载cookies并检查登录状态
|
||||||
|
if err := p.LoadCookies(); err == nil {
|
||||||
|
p.Page.MustNavigate(p.EditorURL)
|
||||||
|
p.WaitForPageReady(5)
|
||||||
|
p.Sleep(2)
|
||||||
|
}
|
||||||
|
// 统一检查登录状态
|
||||||
|
if !p.CheckLoginStatus() {
|
||||||
|
p.LogInfo("未登录或登录已过期,需要重新登录")
|
||||||
|
return fmt.Errorf("需要登录")
|
||||||
|
}
|
||||||
|
p.SaveCookies()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// PublishNote 发布文章
|
||||||
|
func (p *WangyiPublisher) 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},
|
||||||
|
{"输入标题", p.inputTitle},
|
||||||
|
{"导入内容", p.importDocument},
|
||||||
|
{"设置封面", p.setCover},
|
||||||
|
{"点击发布", p.clickPublish},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, step := range steps {
|
||||||
|
if err := step.fn(); err != nil {
|
||||||
|
// 失败时截图
|
||||||
|
screenshotFile := fmt.Sprintf("wangyi_screenshot_%s_%d.png", step.name, time.Now().Unix())
|
||||||
|
p.Screenshot(screenshotFile)
|
||||||
|
p.LogStep(step.name, false, err.Error())
|
||||||
|
return false, fmt.Sprintf("%s失败: %v", step.name, err)
|
||||||
|
}
|
||||||
|
p.LogStep(step.name, true, "")
|
||||||
|
p.SleepMs(500)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 等待发布结果
|
||||||
|
success, message := p.waitForPublishResult()
|
||||||
|
if success {
|
||||||
|
p.LogInfo(fmt.Sprintf("🎉 发布成功!%s", message))
|
||||||
|
} else {
|
||||||
|
p.LogError(fmt.Sprintf("发布失败: %s", message))
|
||||||
|
}
|
||||||
|
return success, message
|
||||||
|
}
|
||||||
|
|
||||||
|
// LogWarning 记录警告日志
|
||||||
|
func (p *WangyiPublisher) LogWarning(message string) {
|
||||||
|
p.Logger.Printf("⚠️ %s", message)
|
||||||
|
}
|
||||||
|
|
@ -69,6 +69,7 @@ func (p *XiaohongshuPublisher) WaitLogin() (bool, string) {
|
||||||
p.LogInfo("登录成功")
|
p.LogInfo("登录成功")
|
||||||
return true, "login_success"
|
return true, "login_success"
|
||||||
}
|
}
|
||||||
|
p.SleepMs(1000)
|
||||||
}
|
}
|
||||||
|
|
||||||
return false, "登录超时"
|
return false, "登录超时"
|
||||||
|
|
@ -404,12 +405,11 @@ func (p *XiaohongshuPublisher) ClickUploadBotton() error {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *XiaohongshuPublisher) InitPage() error {
|
func (p *XiaohongshuPublisher) InitPage() error {
|
||||||
// 访问发布页面
|
|
||||||
p.Page.MustNavigate(p.EditorURL)
|
|
||||||
p.WaitForPageReady(5)
|
|
||||||
// 尝试加载cookies并检查登录状态
|
// 尝试加载cookies并检查登录状态
|
||||||
if err := p.LoadCookies(); err == nil {
|
if err := p.LoadCookies(); err == nil {
|
||||||
p.RefreshPage()
|
p.Page.MustNavigate(p.EditorURL)
|
||||||
|
p.WaitForPageReady(5)
|
||||||
p.Sleep(2)
|
p.Sleep(2)
|
||||||
}
|
}
|
||||||
// 统一检查登录状态
|
// 统一检查登录状态
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,384 @@
|
||||||
|
package publisher
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"geo/internal/config"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/go-rod/rod"
|
||||||
|
"github.com/go-rod/rod/lib/proto"
|
||||||
|
)
|
||||||
|
|
||||||
|
type XiaohongshuVideoPublisher struct {
|
||||||
|
*BasePublisher
|
||||||
|
shortWait int
|
||||||
|
mediumWait int
|
||||||
|
longWait int
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewXiaohongshuVideoPublisher(task *TaskParams, cfg *config.Config, logger *log.Logger) PublisherInerface {
|
||||||
|
return &XiaohongshuVideoPublisher{
|
||||||
|
BasePublisher: NewBasePublisher(task, cfg, logger),
|
||||||
|
shortWait: 1,
|
||||||
|
mediumWait: 3,
|
||||||
|
longWait: 5,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *XiaohongshuVideoPublisher) 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 *XiaohongshuVideoPublisher) WaitLogin() (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)
|
||||||
|
|
||||||
|
if p.CheckLoginStatus() {
|
||||||
|
p.SaveCookies()
|
||||||
|
return true, "already_logged_in"
|
||||||
|
}
|
||||||
|
|
||||||
|
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(1000)
|
||||||
|
}
|
||||||
|
|
||||||
|
return false, "登录超时,请检查网络或账号状态"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *XiaohongshuVideoPublisher) waitForEditorReady(timeout int) bool {
|
||||||
|
p.LogInfo("等待编辑器加载...")
|
||||||
|
startTime := time.Now()
|
||||||
|
for time.Since(startTime) < time.Duration(timeout)*time.Second {
|
||||||
|
uploadArea, err := p.Page.Element(".upload-wrapper")
|
||||||
|
if err == nil && uploadArea != nil {
|
||||||
|
p.LogInfo("编辑器加载完成")
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
p.SleepMs(1000)
|
||||||
|
}
|
||||||
|
p.LogInfo("编辑器加载超时")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *XiaohongshuVideoPublisher) uploadVideo() error {
|
||||||
|
if p.SourcePath == "" {
|
||||||
|
return fmt.Errorf("视频不存在")
|
||||||
|
}
|
||||||
|
if _, err := os.Stat(p.SourcePath); os.IsNotExist(err) {
|
||||||
|
return fmt.Errorf("视频不存在")
|
||||||
|
}
|
||||||
|
|
||||||
|
p.LogInfo(fmt.Sprintf("开始上传视频: %s", p.SourcePath))
|
||||||
|
|
||||||
|
fileInputSelectors := []string{
|
||||||
|
"input[type='file'][accept*='video']",
|
||||||
|
".upload-input",
|
||||||
|
"input[accept*='mp4']",
|
||||||
|
"input[accept*='video/*']",
|
||||||
|
}
|
||||||
|
|
||||||
|
fileInput, err := p.Page.Element(".upload-input")
|
||||||
|
if err != nil {
|
||||||
|
fmt.Errorf("找到文件上传输入框")
|
||||||
|
}
|
||||||
|
|
||||||
|
if fileInput == nil {
|
||||||
|
uploadArea, err := p.WaitForElementVisible(".video-plugin-title-action", 5)
|
||||||
|
if err == nil && uploadArea != nil {
|
||||||
|
p.LogInfo("点击上传区域")
|
||||||
|
p.JSClick(uploadArea)
|
||||||
|
p.SleepMs(1000)
|
||||||
|
|
||||||
|
for _, selector := range fileInputSelectors {
|
||||||
|
fileInput, _ = p.Page.Element(selector)
|
||||||
|
if fileInput != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if fileInput == nil {
|
||||||
|
return fmt.Errorf("未找到文件上传输入框")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := fileInput.SetFiles([]string{p.SourcePath}); err != nil {
|
||||||
|
return fmt.Errorf("上传视频失败: %v", err)
|
||||||
|
}
|
||||||
|
p.LogInfo(fmt.Sprintf("视频文件已选择: %s", p.SourcePath))
|
||||||
|
|
||||||
|
return p.waitForUploadComplete()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *XiaohongshuVideoPublisher) waitForUploadComplete() error {
|
||||||
|
p.LogInfo("等待视频上传完成...")
|
||||||
|
|
||||||
|
for i := 0; i < 300; i++ {
|
||||||
|
publishBtn, err := p.Page.Element(".publish-page-publish-btn button.bg-red")
|
||||||
|
if err == nil && publishBtn != nil {
|
||||||
|
text, _ := publishBtn.Text()
|
||||||
|
if text == "发布" {
|
||||||
|
p.LogInfo("发布按钮已可点击,视频上传完成")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
errorElem, err := p.Page.Element("[class*='error'], .toast-error")
|
||||||
|
if err == nil && errorElem != nil {
|
||||||
|
visible, _ := errorElem.Visible()
|
||||||
|
if visible {
|
||||||
|
text, _ := errorElem.Text()
|
||||||
|
if text != "" {
|
||||||
|
return fmt.Errorf("上传失败: %s", text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
p.SleepMs(1000)
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf("上传超时")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *XiaohongshuVideoPublisher) inputTitle() error {
|
||||||
|
p.LogInfo(fmt.Sprintf("输入视频标题: %s", p.Title))
|
||||||
|
|
||||||
|
titleSelectors := []string{
|
||||||
|
"textarea.d-input",
|
||||||
|
".d-input input",
|
||||||
|
"input[type='text']",
|
||||||
|
"textarea",
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, selector := range titleSelectors {
|
||||||
|
titleInput, err := p.WaitForElementVisible(selector, 5)
|
||||||
|
if err == nil && titleInput != nil {
|
||||||
|
p.LogInfo(fmt.Sprintf("找到标题输入框: %s", selector))
|
||||||
|
|
||||||
|
p.ClearInput(titleInput)
|
||||||
|
p.SleepMs(300)
|
||||||
|
titleInput.Input("")
|
||||||
|
p.SleepMs(300)
|
||||||
|
titleInput.Input(p.Title)
|
||||||
|
|
||||||
|
p.SleepMs(500)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf("未找到标题输入框")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *XiaohongshuVideoPublisher) inputDescription() error {
|
||||||
|
p.LogInfo("输入视频描述...")
|
||||||
|
|
||||||
|
fullDescription := p.Content
|
||||||
|
if len(p.Tags) > 0 {
|
||||||
|
tagStr := ""
|
||||||
|
for _, tag := range p.Tags {
|
||||||
|
if tag != "" {
|
||||||
|
tagStr += fmt.Sprintf("#%s ", tag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tagStr = strings.TrimSpace(tagStr)
|
||||||
|
if fullDescription != "" {
|
||||||
|
fullDescription = fmt.Sprintf("%s\n\n%s", tagStr, fullDescription)
|
||||||
|
} else {
|
||||||
|
fullDescription = tagStr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
p.LogInfo(fmt.Sprintf("描述内容: %s...", fullDescription[:min(len(fullDescription), 100)]))
|
||||||
|
|
||||||
|
editorSelectors := []string{
|
||||||
|
".tiptap.ProseMirror",
|
||||||
|
".ProseMirror",
|
||||||
|
"[contenteditable='true']",
|
||||||
|
".editor-content .tiptap",
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, selector := range editorSelectors {
|
||||||
|
editor, err := p.WaitForElementVisible(selector, 5)
|
||||||
|
if err == nil && editor != nil {
|
||||||
|
p.LogInfo(fmt.Sprintf("找到编辑器: %s", selector))
|
||||||
|
|
||||||
|
editor.Click(proto.InputMouseButtonLeft, 1)
|
||||||
|
p.SleepMs(500)
|
||||||
|
|
||||||
|
p.SetContentEditable(editor, fullDescription)
|
||||||
|
|
||||||
|
p.SleepMs(1000)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf("未找到描述输入框")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *XiaohongshuVideoPublisher) clickPublish() error {
|
||||||
|
p.LogInfo("点击发布按钮...")
|
||||||
|
|
||||||
|
p.Page.Eval(`() => window.scrollTo(0, document.body.scrollHeight)`)
|
||||||
|
p.SleepMs(1000)
|
||||||
|
|
||||||
|
var publishBtn *rod.Element
|
||||||
|
|
||||||
|
publishContainer, err := p.WaitForElementVisible(".publish-page-publish-btn", 5)
|
||||||
|
if err == nil && publishContainer != nil {
|
||||||
|
buttons, _ := publishContainer.Elements("button")
|
||||||
|
for _, btn := range buttons {
|
||||||
|
text, _ := btn.Text()
|
||||||
|
if text == "发布" {
|
||||||
|
publishBtn = btn
|
||||||
|
p.LogInfo("找到发布按钮")
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if publishBtn == nil {
|
||||||
|
allButtons, _ := p.Page.Elements("button")
|
||||||
|
for _, btn := range allButtons {
|
||||||
|
text, _ := btn.Text()
|
||||||
|
if text == "发布" && !strings.Contains(text, "暂存") {
|
||||||
|
publishBtn = btn
|
||||||
|
p.LogInfo("通过遍历找到发布按钮")
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if publishBtn == nil {
|
||||||
|
return fmt.Errorf("未找到发布按钮")
|
||||||
|
}
|
||||||
|
|
||||||
|
publishBtn.ScrollIntoView()
|
||||||
|
p.SleepMs(500)
|
||||||
|
|
||||||
|
if err := publishBtn.Click(proto.InputMouseButtonLeft, 1); err != nil {
|
||||||
|
p.JSClick(publishBtn)
|
||||||
|
p.LogInfo("使用JS点击发布按钮")
|
||||||
|
} else {
|
||||||
|
p.LogInfo("已点击发布按钮")
|
||||||
|
}
|
||||||
|
|
||||||
|
p.SleepMs(2000)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *XiaohongshuVideoPublisher) waitForPublishResult(timeout int) (bool, string) {
|
||||||
|
p.LogInfo("等待发布结果...")
|
||||||
|
|
||||||
|
startTime := time.Now()
|
||||||
|
for time.Since(startTime) < time.Duration(timeout)*time.Second {
|
||||||
|
currentURL := p.GetCurrentURL()
|
||||||
|
|
||||||
|
successKeywords := []string{"success", "content/manage", "work-management"}
|
||||||
|
for _, keyword := range successKeywords {
|
||||||
|
if strings.Contains(currentURL, keyword) {
|
||||||
|
p.LogInfo(fmt.Sprintf("发布成功!URL: %s", currentURL))
|
||||||
|
return true, "发布成功"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
toasts, _ := p.Page.Elements(".semi-toast-content, [class*='toast']")
|
||||||
|
for _, toast := range toasts {
|
||||||
|
visible, _ := toast.Visible()
|
||||||
|
if visible {
|
||||||
|
text, _ := toast.Text()
|
||||||
|
if strings.Contains(text, "成功") || strings.Contains(text, "已发布") {
|
||||||
|
p.LogInfo(fmt.Sprintf("发布成功: %s", text))
|
||||||
|
return true, text
|
||||||
|
} else if strings.Contains(text, "失败") {
|
||||||
|
p.LogError(fmt.Sprintf("发布失败: %s", text))
|
||||||
|
return false, text
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.Contains(currentURL, "publish") && time.Since(startTime) > 10*time.Second {
|
||||||
|
errorMsgs, _ := p.Page.Elements("[class*='error'], .toast-error")
|
||||||
|
for _, elem := range errorMsgs {
|
||||||
|
visible, _ := elem.Visible()
|
||||||
|
if visible {
|
||||||
|
text, _ := elem.Text()
|
||||||
|
if text != "" {
|
||||||
|
return false, text
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
p.SleepMs(2000)
|
||||||
|
}
|
||||||
|
|
||||||
|
return false, "发布结果未知(超时)"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *XiaohongshuVideoPublisher) 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},
|
||||||
|
{"上传视频", p.uploadVideo},
|
||||||
|
{"输入标题", p.inputTitle},
|
||||||
|
{"输入描述", p.inputDescription},
|
||||||
|
{"点击发布", p.clickPublish},
|
||||||
|
}
|
||||||
|
|
||||||
|
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(1000)
|
||||||
|
}
|
||||||
|
|
||||||
|
success, message := p.waitForPublishResult(60)
|
||||||
|
if success {
|
||||||
|
p.LogInfo(fmt.Sprintf("🎉 发布成功!%s", message))
|
||||||
|
return true, message
|
||||||
|
}
|
||||||
|
p.LogError(fmt.Sprintf("发布失败: %s", message))
|
||||||
|
return false, message
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,410 @@
|
||||||
|
package publisher
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"geo/internal/config"
|
||||||
|
"log"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/go-rod/rod"
|
||||||
|
"github.com/go-rod/rod/lib/proto"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ZhihuPublisher struct {
|
||||||
|
*BasePublisher
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewZhihuPublisher(task *TaskParams, cfg *config.Config, logger *log.Logger) PublisherInerface {
|
||||||
|
return &ZhihuPublisher{NewBasePublisher(task, cfg, logger)}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *ZhihuPublisher) CheckLogin() (bool, string) {
|
||||||
|
p.LogInfo("检查登录状态...")
|
||||||
|
|
||||||
|
if err := p.SetupDriver(); err != nil {
|
||||||
|
return false, fmt.Sprintf("浏览器启动失败: %v", err)
|
||||||
|
}
|
||||||
|
defer p.Close()
|
||||||
|
|
||||||
|
p.Page.MustNavigate(p.EditorURL)
|
||||||
|
p.Sleep(3)
|
||||||
|
p.WaitForPageReady(5)
|
||||||
|
|
||||||
|
if p.CheckLoginStatus() {
|
||||||
|
p.SaveCookies()
|
||||||
|
return true, "已登录"
|
||||||
|
}
|
||||||
|
return false, "未登录"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *ZhihuPublisher) CheckLoginStatus() bool {
|
||||||
|
currentURL := p.GetCurrentURL()
|
||||||
|
if strings.Contains(currentURL, p.LoginURL) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *ZhihuPublisher) WaitLogin() (bool, string) {
|
||||||
|
p.LogInfo("开始等待登录...")
|
||||||
|
|
||||||
|
if err := p.SetupDriver(); err != nil {
|
||||||
|
return false, fmt.Sprintf("浏览器启动失败: %v", err)
|
||||||
|
}
|
||||||
|
defer p.Close()
|
||||||
|
|
||||||
|
if p.EditorURL != "" {
|
||||||
|
p.Page.MustNavigate(p.EditorURL)
|
||||||
|
p.Sleep(3)
|
||||||
|
|
||||||
|
if p.CheckLoginStatus() {
|
||||||
|
p.SaveCookies()
|
||||||
|
return true, "already_logged_in"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
startTime := time.Now()
|
||||||
|
timeout := 240
|
||||||
|
for time.Since(startTime) < time.Duration(timeout)*time.Second {
|
||||||
|
currentURL := p.GetCurrentURL()
|
||||||
|
if p.EditorURL != "" && strings.Contains(currentURL, p.EditorURL) {
|
||||||
|
p.SaveCookies()
|
||||||
|
return true, "login_success"
|
||||||
|
}
|
||||||
|
if p.LoginedURL != "" && strings.Contains(currentURL, p.LoginedURL) {
|
||||||
|
p.SaveCookies()
|
||||||
|
return true, "login_success"
|
||||||
|
}
|
||||||
|
if p.CheckLoginStatus() {
|
||||||
|
p.SaveCookies()
|
||||||
|
return true, "login_success"
|
||||||
|
}
|
||||||
|
p.SleepMs(1000)
|
||||||
|
}
|
||||||
|
|
||||||
|
return false, "登录超时,请检查网络或账号状态"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *ZhihuPublisher) waitForEditorReady(timeout int) bool {
|
||||||
|
p.LogInfo("等待编辑器加载...")
|
||||||
|
startTime := time.Now()
|
||||||
|
for time.Since(startTime) < time.Duration(timeout)*time.Second {
|
||||||
|
titleSelectors := []string{
|
||||||
|
".WriteIndex-titleInput textarea",
|
||||||
|
".DraftEditor-root",
|
||||||
|
".public-DraftEditor-content",
|
||||||
|
"[contenteditable='true']",
|
||||||
|
}
|
||||||
|
for _, selector := range titleSelectors {
|
||||||
|
el, err := p.Page.Element(selector)
|
||||||
|
if err == nil && el != nil {
|
||||||
|
visible, _ := el.Visible()
|
||||||
|
if visible {
|
||||||
|
p.LogInfo("编辑器加载完成")
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
p.SleepMs(1000)
|
||||||
|
}
|
||||||
|
p.LogInfo("编辑器加载超时")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *ZhihuPublisher) inputTitle() error {
|
||||||
|
p.LogInfo("输入文章标题...")
|
||||||
|
|
||||||
|
titleSelectors := []string{
|
||||||
|
".WriteIndex-titleInput textarea",
|
||||||
|
"textarea[placeholder*='标题']",
|
||||||
|
".Input[placeholder*='标题']",
|
||||||
|
".title-input textarea",
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, selector := range titleSelectors {
|
||||||
|
titleInput, err := p.WaitForElementVisible(selector, 5)
|
||||||
|
if err == nil && titleInput != nil {
|
||||||
|
p.LogInfo(fmt.Sprintf("找到标题输入框: %s", selector))
|
||||||
|
|
||||||
|
p.ClearInput(titleInput)
|
||||||
|
p.SleepMs(300)
|
||||||
|
titleInput.Input("")
|
||||||
|
p.SleepMs(300)
|
||||||
|
titleInput.Input(p.Title)
|
||||||
|
|
||||||
|
p.LogInfo(fmt.Sprintf("标题已输入: %s", p.Title))
|
||||||
|
|
||||||
|
titleInput.Evaluate(&rod.EvalOptions{
|
||||||
|
JS: `(el) => {
|
||||||
|
el.dispatchEvent(new Event('input', {bubbles: true}));
|
||||||
|
el.dispatchEvent(new Event('change', {bubbles: true}));
|
||||||
|
el.dispatchEvent(new Event('blur', {bubbles: true}));
|
||||||
|
}`,
|
||||||
|
})
|
||||||
|
p.SleepMs(500)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf("未找到标题输入框")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *ZhihuPublisher) importDocument() error {
|
||||||
|
if p.SourcePath == "" {
|
||||||
|
p.LogInfo("未提供文档路径或文档不存在")
|
||||||
|
return fmt.Errorf("文档不存在")
|
||||||
|
}
|
||||||
|
|
||||||
|
p.LogInfo(fmt.Sprintf("尝试导入文档: %s", p.SourcePath))
|
||||||
|
|
||||||
|
// 步骤1: 查找并点击"导入"按钮
|
||||||
|
p.LogInfo("步骤1: 查找并点击'导入'按钮...")
|
||||||
|
|
||||||
|
importBtn, err := p.Page.Element("button[aria-label='导入']")
|
||||||
|
if err != nil {
|
||||||
|
buttons, _ := p.Page.Elements("button")
|
||||||
|
for _, btn := range buttons {
|
||||||
|
text, _ := btn.Text()
|
||||||
|
if text == "导入" {
|
||||||
|
importBtn = btn
|
||||||
|
p.LogInfo("通过文本找到导入按钮")
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if importBtn == nil {
|
||||||
|
return fmt.Errorf("未找到导入按钮")
|
||||||
|
}
|
||||||
|
|
||||||
|
importBtn.Click(proto.InputMouseButtonLeft, 1)
|
||||||
|
p.LogInfo("已点击导入按钮")
|
||||||
|
p.SleepMs(1000)
|
||||||
|
|
||||||
|
// 步骤2: 查找并点击"导入文档"按钮
|
||||||
|
p.LogInfo("步骤2: 查找并点击'导入文档'按钮...")
|
||||||
|
|
||||||
|
docImportBtn, err := p.Page.Element("button[aria-label='导入文档']")
|
||||||
|
if err != nil {
|
||||||
|
menuButtons, _ := p.Page.Elements(".Menu button, .Popover-content button")
|
||||||
|
for _, btn := range menuButtons {
|
||||||
|
text, _ := btn.Text()
|
||||||
|
if strings.Contains(text, "导入文档") {
|
||||||
|
docImportBtn = btn
|
||||||
|
p.LogInfo("通过文本找到导入文档按钮")
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if docImportBtn == nil {
|
||||||
|
return fmt.Errorf("未找到导入文档按钮")
|
||||||
|
}
|
||||||
|
|
||||||
|
docImportBtn.Click(proto.InputMouseButtonLeft, 1)
|
||||||
|
p.LogInfo("已点击导入文档按钮")
|
||||||
|
p.SleepMs(1000)
|
||||||
|
|
||||||
|
// 步骤3: 查找file input并上传文档
|
||||||
|
p.LogInfo("步骤3: 查找文件上传输入框...")
|
||||||
|
|
||||||
|
var fileInput *rod.Element
|
||||||
|
for i := 0; i < 10; i++ {
|
||||||
|
fileInput, _ = p.Page.Element("input[type='file'][accept='.docx,.markdown,.mdown,.mkdn,.md']")
|
||||||
|
if fileInput != nil {
|
||||||
|
p.LogInfo("找到导入文档输入框")
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if fileInput == nil {
|
||||||
|
hiddenInputs, _ := p.Page.Elements("input[type='file'][style*='display: none']")
|
||||||
|
for _, inp := range hiddenInputs {
|
||||||
|
accept, _ := inp.Attribute("accept")
|
||||||
|
if accept != nil && (strings.Contains(*accept, ".docx") || strings.Contains(*accept, ".md")) {
|
||||||
|
fileInput = inp
|
||||||
|
p.LogInfo("找到隐藏的导入文档输入框")
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if fileInput != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
p.SleepMs(500)
|
||||||
|
}
|
||||||
|
|
||||||
|
if fileInput == nil {
|
||||||
|
return fmt.Errorf("未找到文件上传输入框")
|
||||||
|
}
|
||||||
|
|
||||||
|
p.LogInfo(fmt.Sprintf("开始上传文档: %s", p.SourcePath))
|
||||||
|
fileInput.SetFiles([]string{p.SourcePath})
|
||||||
|
p.LogInfo(fmt.Sprintf("文档已上传: %s", p.SourcePath))
|
||||||
|
p.Sleep(3)
|
||||||
|
|
||||||
|
// 等待导入完成
|
||||||
|
success := false
|
||||||
|
for i := 0; i < 60; i++ {
|
||||||
|
p.SleepMs(1000)
|
||||||
|
|
||||||
|
toasts, _ := p.Page.Elements(".Toast-module_toast, .el-message--success, .toast-success")
|
||||||
|
for _, toast := range toasts {
|
||||||
|
visible, _ := toast.Visible()
|
||||||
|
if visible {
|
||||||
|
text, _ := toast.Text()
|
||||||
|
if strings.Contains(text, "成功") || strings.Contains(text, "导入") || strings.Contains(text, "完成") {
|
||||||
|
p.LogInfo(fmt.Sprintf("导入成功提示: %s", text))
|
||||||
|
success = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if success {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
editors, _ := p.Page.Elements("[contenteditable='true'], .DraftEditor-root, .ProseMirror")
|
||||||
|
for _, editor := range editors {
|
||||||
|
text, _ := editor.Text()
|
||||||
|
if len(text) > 10 {
|
||||||
|
p.LogInfo(fmt.Sprintf("文档导入成功,内容长度: %d", len(text)))
|
||||||
|
success = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if success {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if success {
|
||||||
|
p.SleepMs(2000)
|
||||||
|
p.LogInfo("文档导入完成")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
p.LogInfo("文档导入状态未知")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *ZhihuPublisher) clickPublish() error {
|
||||||
|
p.LogInfo("点击发布按钮...")
|
||||||
|
|
||||||
|
publishSelectors := []string{
|
||||||
|
".Button--primary:contains('发布')",
|
||||||
|
".css-d0uhtl",
|
||||||
|
"button:contains('发布')",
|
||||||
|
".PublishButton",
|
||||||
|
"[class*='publish'] button.Button--primary",
|
||||||
|
}
|
||||||
|
|
||||||
|
var publishBtn *rod.Element
|
||||||
|
for _, selector := range publishSelectors {
|
||||||
|
publishBtn, _ = p.WaitForElementClickable(selector, 5)
|
||||||
|
if publishBtn != nil {
|
||||||
|
visible, _ := publishBtn.Visible()
|
||||||
|
if visible {
|
||||||
|
p.LogInfo(fmt.Sprintf("找到发布按钮: %s", selector))
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if publishBtn == nil {
|
||||||
|
buttons, _ := p.Page.Elements("button")
|
||||||
|
for _, btn := range buttons {
|
||||||
|
visible, _ := btn.Visible()
|
||||||
|
if visible {
|
||||||
|
text, _ := btn.Text()
|
||||||
|
if text == "发布" || strings.Contains(text, "发布") {
|
||||||
|
publishBtn = btn
|
||||||
|
p.LogInfo("通过遍历按钮找到发布按钮")
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if publishBtn == nil {
|
||||||
|
return fmt.Errorf("未找到发布按钮")
|
||||||
|
}
|
||||||
|
|
||||||
|
p.SleepMs(500)
|
||||||
|
|
||||||
|
if err := publishBtn.Click(proto.InputMouseButtonLeft, 1); err != nil {
|
||||||
|
if err := p.JSClick(publishBtn); err != nil {
|
||||||
|
return fmt.Errorf("点击发布按钮失败: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
p.LogInfo("已点击发布按钮")
|
||||||
|
p.Sleep(3)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *ZhihuPublisher) waitForPublishResult(timeout int) (bool, string) {
|
||||||
|
p.LogInfo("等待发布结果...")
|
||||||
|
|
||||||
|
startTime := time.Now()
|
||||||
|
for time.Since(startTime) < time.Duration(timeout)*time.Second {
|
||||||
|
currentURL := p.GetCurrentURL()
|
||||||
|
|
||||||
|
// 检查失败弹窗
|
||||||
|
failedDiv, err := p.Page.Element(".Notification-textSection")
|
||||||
|
if err == nil && failedDiv != nil {
|
||||||
|
visible, _ := failedDiv.Visible()
|
||||||
|
if visible {
|
||||||
|
failedReason, _ := failedDiv.Text()
|
||||||
|
p.LogInfo(fmt.Sprintf("发布失败: %s", failedReason))
|
||||||
|
return false, failedReason
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
successKeywords := []string{"/p/", "/article/", "/people/", "/column/"}
|
||||||
|
for _, keyword := range successKeywords {
|
||||||
|
if strings.Contains(currentURL, keyword) && !strings.Contains(currentURL, "write") {
|
||||||
|
p.LogInfo(fmt.Sprintf("发布成功!URL: %s", currentURL))
|
||||||
|
return true, "发布成功"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
p.SleepMs(1000)
|
||||||
|
}
|
||||||
|
|
||||||
|
return false, "发布结果未知(超时)"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *ZhihuPublisher) 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},
|
||||||
|
{"输入标题", p.inputTitle},
|
||||||
|
{"导入内容", p.importDocument},
|
||||||
|
{"点击发布", p.clickPublish},
|
||||||
|
}
|
||||||
|
|
||||||
|
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, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
success, message := p.waitForPublishResult(60)
|
||||||
|
if success {
|
||||||
|
p.LogInfo(fmt.Sprintf("🎉 发布成功!%s", message))
|
||||||
|
return true, message
|
||||||
|
}
|
||||||
|
p.LogError(fmt.Sprintf("发布失败: %s", message))
|
||||||
|
return false, message
|
||||||
|
}
|
||||||
49
pkg/func.go
49
pkg/func.go
|
|
@ -176,18 +176,55 @@ func CopyNonNilFields(src, dst interface{}) error {
|
||||||
func DownloadFile(url string, saveDir string, filename string) (string, error) {
|
func DownloadFile(url string, saveDir string, filename string) (string, error) {
|
||||||
os.MkdirAll(saveDir, 0755)
|
os.MkdirAll(saveDir, 0755)
|
||||||
|
|
||||||
if filename == "" {
|
|
||||||
filename = uuid.New().String() + ".docx"
|
|
||||||
}
|
|
||||||
|
|
||||||
filePath := filepath.Join(saveDir, filename)
|
|
||||||
|
|
||||||
resp, err := http.Get(url)
|
resp, err := http.Get(url)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
// 获取文件扩展名
|
||||||
|
ext := ""
|
||||||
|
|
||||||
|
// 1. 从URL获取扩展名
|
||||||
|
if idx := strings.LastIndex(url, "."); idx != -1 {
|
||||||
|
ext = url[idx:]
|
||||||
|
// 去除查询参数
|
||||||
|
if idx2 := strings.Index(ext, "?"); idx2 != -1 {
|
||||||
|
ext = ext[:idx2]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 如果URL没有扩展名,从Content-Type获取
|
||||||
|
if ext == "" {
|
||||||
|
contentType := resp.Header.Get("Content-Type")
|
||||||
|
switch contentType {
|
||||||
|
case "application/vnd.openxmlformats-officedocument.wordprocessingml.document":
|
||||||
|
ext = ".docx"
|
||||||
|
case "application/msword":
|
||||||
|
ext = ".doc"
|
||||||
|
case "application/pdf":
|
||||||
|
ext = ".pdf"
|
||||||
|
case "video/mp4":
|
||||||
|
ext = ".mp4"
|
||||||
|
case "video/x-msvideo":
|
||||||
|
ext = ".avi"
|
||||||
|
default:
|
||||||
|
// 默认空,后续会报错
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ext == "" {
|
||||||
|
return "", fmt.Errorf("无法确定文件类型: %s", url)
|
||||||
|
}
|
||||||
|
|
||||||
|
if filename == "" {
|
||||||
|
filename = uuid.New().String() + ext
|
||||||
|
} else if !strings.HasSuffix(filename, ext) {
|
||||||
|
filename = filename + ext
|
||||||
|
}
|
||||||
|
|
||||||
|
filePath := filepath.Join(saveDir, filename)
|
||||||
|
|
||||||
out, err := os.Create(filePath)
|
out, err := os.Create(filePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue