package publisher import ( "context" "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(ctx context.Context, task *TaskParams, cfg *config.Config, logger *log.Logger) PublisherInerface { return &ZhihuPublisher{NewBasePublisher(ctx, 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.Page.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() if !strings.Contains(currentURL, "/edit") { p.LogInfo(fmt.Sprintf("发布成功!URL: %s", currentURL)) return true, "发布成功" } // 检查失败弹窗 exist, failedDiv, _ := p.Page.HasX(".Notification-textSection") if exist { failedReason, _ := failedDiv.Text() p.LogInfo(fmt.Sprintf("发布失败: %s", failedReason)) return false, failedReason } 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 }