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})
|
||||
tokenInfo := &model.Token{}
|
||||
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 tokenInfo, nil
|
||||
|
|
|
|||
|
|
@ -201,8 +201,8 @@ func (p *PublishImpl) GetListWithUser(ctx context.Context, tokenID int32, page,
|
|||
p.create_time,
|
||||
p.msg
|
||||
`).
|
||||
Joins("LEFT JOIN user u ON p.user_index = u.user_index").
|
||||
Joins("LEFT JOIN plat pl ON p.plat_index = pl.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 COLLATE utf8mb4_unicode_ci = pl.index").
|
||||
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) {
|
||||
// 下载文档
|
||||
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 {
|
||||
return "", "", fmt.Errorf("下载文档失败: %v", err)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -104,13 +104,9 @@ func (p *BaijiahaoPublisher) PublishNote() (bool, string) {
|
|||
}
|
||||
driverCreated = true
|
||||
|
||||
p.Page.MustNavigate(p.EditorURL)
|
||||
p.Sleep(3)
|
||||
p.WaitForPageReady(5)
|
||||
|
||||
if p.LoadCookies() == nil {
|
||||
p.RefreshPage()
|
||||
p.Sleep(3)
|
||||
p.Page.MustNavigate(p.EditorURL)
|
||||
p.WaitForPageReady(5)
|
||||
if p.CheckLoginStatus() {
|
||||
return p.doPublish()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -127,6 +127,9 @@ func (b *BasePublisher) SetupDriver() error {
|
|||
os.MkdirAll(userDataDir, 0755)
|
||||
l.UserDataDir(userDataDir)
|
||||
|
||||
// 关键优化:不重新使用已有的数据目录时不要清除
|
||||
l.Set("profile-directory", "Default")
|
||||
|
||||
// 设置 Leakless 模式(解决 Windows 上的问题)
|
||||
l.Leakless(false)
|
||||
|
||||
|
|
@ -294,8 +297,12 @@ func (b *BasePublisher) WaitLogin() (bool, string) {
|
|||
return false, "需要实现"
|
||||
}
|
||||
|
||||
func (b *BasePublisher) CheckLoginStatus() bool {
|
||||
return false
|
||||
func (p *BasePublisher) CheckLoginStatus() bool {
|
||||
currentURL := p.GetCurrentURL()
|
||||
if strings.Contains(currentURL, p.LoginURL) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (b *BasePublisher) CheckLogin() (bool, string) {
|
||||
|
|
@ -354,3 +361,21 @@ func (b *BasePublisher) StartNote() {
|
|||
b.LogInfo(fmt.Sprintf("标签: %v", b.Tags))
|
||||
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,
|
||||
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
|
||||
}
|
||||
|
||||
// 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 检查弹窗是否已关闭
|
||||
func (p *ToutiaoPublisher) isDrawerClosed() bool {
|
||||
drawerWrappers, err := p.Page.Elements(".byte-drawer-wrapper")
|
||||
|
|
@ -462,7 +393,7 @@ func (p *ToutiaoPublisher) clickPublish() error {
|
|||
func (p *ToutiaoPublisher) waitForPublishResult() (bool, string) {
|
||||
p.LogInfo("等待发布结果...")
|
||||
|
||||
for attempt := 0; attempt < 60; attempt++ {
|
||||
for attempt := 0; attempt < p.MaxRetries; attempt++ {
|
||||
currentURL := p.GetCurrentURL()
|
||||
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("发布结果未知")
|
||||
|
|
@ -519,14 +450,11 @@ func (p *ToutiaoPublisher) waitForPublishResult() (bool, string) {
|
|||
// InitPage 初始化页面
|
||||
func (p *ToutiaoPublisher) InitPage() error {
|
||||
// 访问发布页面
|
||||
p.Page.MustNavigate(p.EditorURL)
|
||||
p.Sleep(2)
|
||||
p.WaitForPageReady(5)
|
||||
|
||||
// 尝试加载cookies并检查登录状态
|
||||
if err := p.LoadCookies(); err == nil {
|
||||
p.RefreshPage()
|
||||
p.Sleep(2)
|
||||
p.Page.MustNavigate(p.EditorURL)
|
||||
p.WaitForPageReady(5)
|
||||
if p.CheckLoginStatus() {
|
||||
p.SaveCookies()
|
||||
return nil
|
||||
|
|
@ -553,7 +481,6 @@ func (p *ToutiaoPublisher) PublishNote() (bool, string) {
|
|||
}{
|
||||
{"初始化页面", p.InitPage},
|
||||
{"输入内容", p.inputContent},
|
||||
//{"上传封面", p.uploadCover},
|
||||
{"输入标题", p.inputTitle},
|
||||
{"点击发布", 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("登录成功")
|
||||
return true, "login_success"
|
||||
}
|
||||
p.SleepMs(1000)
|
||||
}
|
||||
|
||||
return false, "登录超时"
|
||||
|
|
@ -404,12 +405,11 @@ func (p *XiaohongshuPublisher) ClickUploadBotton() error {
|
|||
}
|
||||
|
||||
func (p *XiaohongshuPublisher) InitPage() error {
|
||||
// 访问发布页面
|
||||
p.Page.MustNavigate(p.EditorURL)
|
||||
p.WaitForPageReady(5)
|
||||
|
||||
// 尝试加载cookies并检查登录状态
|
||||
if err := p.LoadCookies(); err == nil {
|
||||
p.RefreshPage()
|
||||
p.Page.MustNavigate(p.EditorURL)
|
||||
p.WaitForPageReady(5)
|
||||
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) {
|
||||
os.MkdirAll(saveDir, 0755)
|
||||
|
||||
if filename == "" {
|
||||
filename = uuid.New().String() + ".docx"
|
||||
}
|
||||
|
||||
filePath := filepath.Join(saveDir, filename)
|
||||
|
||||
resp, err := http.Get(url)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
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)
|
||||
if err != nil {
|
||||
return "", err
|
||||
|
|
|
|||
Loading…
Reference in New Issue