// douyin 抖音执行器(CDP:creator.douyin.com)。选择器参考 dreammis/social-auto-upload(2026-06 版)。 package douyin import ( "context" "time" "everypublish/agentcore/internal/exec" "everypublish/agentcore/internal/exec/cdnbase" "everypublish/agentcore/internal/exec/cdp" "everypublish/agentcore/internal/vault" "everypublish/shared/proto" ) // Executor 抖音发布执行器 type Executor struct { base *cdnbase.Platform } // New 构造执行器(rootDir=浏览器档案根目录,proxy=账号固定IP代理) func New(v *vault.Vault, rootDir, proxy, chromePath string, headless bool) *Executor { p := &cdnbase.Platform{ Vault: v, RootDir: rootDir, Proxy: proxy, ChromePath: chromePath, Headless: headless, ReceiptDir: rootDir + "/receipts", Name: "douyin", LoginDomain: ".douyin.com", PublishCfg: cdp.PublishConfig{ UploadURL: "https://creator.douyin.com/creator-micro/content/upload", SuccessMarker: "creator-micro/content/manage", FileInputSel: "input[type=\"file\"]", TitleSel: "input[placeholder*=\"填写作品标题\"]", DescSel: "div.zone-container[contenteditable=\"true\"]", PublishBtnSel: "button:has-text(\"发布\")", FormWait: 180 * time.Second, // 视频转码渲染表单较慢 SuccessWait: 120 * time.Second, }, LoginCfg: cdp.QRLoginConfig{ LoginURL: "https://creator.douyin.com/", LoginPathMarker: "creator.douyin.com/?", QRSelector: ".qrcode-v2 img, .qrcode img, img[src*=qrcode]", SuccessWait: 3 * time.Minute, }, } return &Executor{base: p} } // Platform 平台标识 func (e *Executor) Platform() string { return "douyin" } // Execute 发布 func (e *Executor) Execute(ctx context.Context, task exec.Task) exec.Result { return e.base.Publish(ctx, task) } // SolveChallenge 扫码登录(QR 截图经 onQR 展示) func (e *Executor) SolveChallenge(ctx context.Context, ch proto.Challenge, onQR func(qrToken, qrURL string)) (bool, string) { return e.base.SolveChallenge(ctx, ch, onQR) }