// Publish 通用发布流程:进上传页 → 注入 cookie → 传视频 → 填表单 → 点发布 → 等成功跳转 → 截图回执。 // 各平台选择器差异经 PublishConfig 配置;平台改版时只需调整配置。 package cdp import ( "context" "fmt" "strings" "time" "github.com/go-rod/rod" "github.com/go-rod/rod/lib/proto" ) // PublishConfig 发布流程配置(选择器按平台填写) type PublishConfig struct { Platform string UploadURL string SuccessMarker string // 发布成功后的 URL 特征 FileInputSel string TitleSel string DescSel string PublishBtnSel string FormWait time.Duration // 等视频处理出表单的最长时间 SuccessWait time.Duration ReceiptDir string } // PublishResult 发布结果 type PublishResult struct { URL string Receipts []string } // Publish 执行通用发布流程 func (b *Browser) Publish(ctx context.Context, cfg PublishConfig, videoPath, title, content string, tags []string, cookieStr, cookieDomain string) (*PublishResult, error) { if err := b.Page.Navigate(cfg.UploadURL); err != nil { return nil, err } _ = b.Page.WaitLoad() // 注入登录 cookie(有则免扫码) if cookieStr != "" { _ = b.InjectCookies(cookieStr, cookieDomain) if err := b.Page.Navigate(cfg.UploadURL); err != nil { return nil, err } _ = b.Page.WaitLoad() } // 1) 传视频 fileInput, err := b.Page.Element(cfg.FileInputSel) if err != nil { return nil, fmt.Errorf("找不到上传入口(%s): %w", cfg.FileInputSel, err) } if err = fileInput.SetFiles([]string{videoPath}); err != nil { return nil, fmt.Errorf("选择文件失败: %w", err) } Log("%s: 视频已选择,等待平台处理...", cfg.Platform) // 2) 等表单渲染(视频转码后出现标题输入框) titleEl, err := b.Page.ElementR(cfg.TitleSel, "") if err != nil { titleEl, err = b.waitElement(ctx, cfg.TitleSel, cfg.FormWait) } if err != nil { b.Screenshot(cfg.ReceiptDir, cfg.Platform+"-form-fail.png") return nil, fmt.Errorf("等表单渲染超时(%s): %w", cfg.TitleSel, err) } // 3) 填标题/描述(话题并入描述) if err = titleEl.Input(title); err != nil { return nil, fmt.Errorf("填标题失败: %w", err) } desc := content if len(tags) > 0 { for _, t := range tags { t = strings.TrimSpace(t) if t != "" && !strings.HasPrefix(t, "#") { t = "#" + t } desc += " " + t } } if descEl, derr := b.Page.Element(cfg.DescSel); derr == nil { _ = descEl.Input(desc) } // 4) 点发布 btn, err := b.waitElement(ctx, cfg.PublishBtnSel, 15*time.Second) if err != nil { b.Screenshot(cfg.ReceiptDir, cfg.Platform+"-btn-fail.png") return nil, fmt.Errorf("找不到发布按钮(%s): %w", cfg.PublishBtnSel, err) } if err = btn.Click(proto.InputMouseButtonLeft, 1); err != nil { return nil, fmt.Errorf("点发布失败: %w", err) } Log("%s: 已点发布,等待结果...", cfg.Platform) // 5) 等成功跳转 if err = b.waitURL(ctx, cfg.SuccessMarker, cfg.SuccessWait); err != nil { b.Screenshot(cfg.ReceiptDir, cfg.Platform+"-publish-fail.png") return nil, err } info, _ := b.Page.Info() receipt := b.Screenshot(cfg.ReceiptDir, cfg.Platform+"-success.png") Log("%s: 发布成功 %s", cfg.Platform, info.URL) return &PublishResult{URL: info.URL, Receipts: []string{receipt}}, nil } // waitElement 轮询等待元素出现 func (b *Browser) waitElement(ctx context.Context, selector string, timeout time.Duration) (*rod.Element, error) { deadline := time.Now().Add(timeout) for time.Now().Before(deadline) { select { case <-ctx.Done(): return nil, fmt.Errorf("cancelled") default: } el, err := b.Page.Element(selector) if err == nil { return el, nil } time.Sleep(2 * time.Second) } return nil, fmt.Errorf("timeout waiting %s", selector) } // waitURL 轮询等待 URL 命中特征 func (b *Browser) waitURL(ctx context.Context, marker string, timeout time.Duration) error { deadline := time.Now().Add(timeout) for time.Now().Before(deadline) { select { case <-ctx.Done(): return fmt.Errorf("cancelled") default: } info, err := b.Page.Info() if err == nil && strings.Contains(info.URL, marker) { return nil } time.Sleep(2 * time.Second) } return fmt.Errorf("timeout waiting url %s", marker) }