This commit is contained in:
renzhiyuan 2026-04-09 03:20:24 +08:00
parent 38e1995f86
commit e2aad42e81
17 changed files with 3652 additions and 99 deletions

View File

@ -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

View File

@ -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)
// 添加过滤条件

View File

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

View File

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

View File

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

220
internal/publisher/csdn.go Normal file
View File

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

427
internal/publisher/dysp.go Normal file
View File

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

View File

@ -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,
},
}

335
internal/publisher/js.go Normal file
View File

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

522
internal/publisher/shh.go Normal file
View File

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

582
internal/publisher/sphsp.go Normal file
View File

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

View File

@ -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},
}

621
internal/publisher/wyh.go Normal file
View File

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

View File

@ -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)
}
// 统一检查登录状态

384
internal/publisher/xhssp.go Normal file
View File

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

410
internal/publisher/zh.go Normal file
View File

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

View File

@ -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