"use strict"; /** * 抖音创作者中心发布(对齐 Electron publishToDouyin,Puppeteer 版) */ const fs = require("fs"); const { delay } = require("./publish_browser.js"); async function clickIfVisible(page, selector) { const el = await page.$(selector); if (!el) return false; try { await el.click(); return true; } catch { return false; } } function isPrimaryPublishLabel(text) { const t = String(text || "").replace(/\s+/g, ""); if (!t.includes("发布")) return false; if (t.includes("定时") || t.includes("草稿") || t.includes("取消")) return false; return ( t === "发布" || t === "确定发布" || t === "立即发布" || t.endsWith("发布") ); } async function clickPublishButton(page) { const handles = await page.$$('button, [role="button"]'); for (const btn of handles) { let text = ""; let visible = false; try { text = await page.evaluate((el) => (el.textContent || "").trim(), btn); visible = await page.evaluate((el) => { const rect = el.getBoundingClientRect(); if (rect.width < 2 || rect.height < 2) return false; const style = window.getComputedStyle(el); return ( style.display !== "none" && style.visibility !== "hidden" && !el.disabled && el.getAttribute("aria-disabled") !== "true" ); }, btn); } catch { continue; } if (!visible || !isPrimaryPublishLabel(text)) continue; try { await btn.evaluate((el) => el.scrollIntoView({ block: "center", inline: "center" }), ); await delay(300); await btn.click(); return true; } catch { /* try next */ } } return false; } async function typeIntoFirst(page, selectors, text) { for (const sel of selectors) { const el = await page.$(sel); if (!el) continue; try { await el.click({ clickCount: 3 }); await page.keyboard.down("Control"); await page.keyboard.press("a"); await page.keyboard.up("Control"); await page.keyboard.press("Backspace"); await el.type(text, { delay: 30 }); return true; } catch { /* try next */ } } return false; } async function publishToDouyin(page, params) { const { videoPath, coverPath, title, tags = [], autoPublish } = params; if (!fs.existsSync(videoPath)) throw new Error("视频文件不存在"); if (!fs.existsSync(coverPath)) throw new Error("封面文件不存在"); globalThis.__native?.emitProgress?.("正在打开抖音上传页…"); const uploadUrl = "https://creator.douyin.com/creator-micro/content/upload"; await page.goto(uploadUrl, { waitUntil: "domcontentloaded" }); await delay(3000); let currentUrl = page.url(); if (currentUrl.includes("login") || currentUrl.includes("passport")) { throw new Error("未登录抖音,请先在步骤 07 点击「登录平台」"); } let avatar = await page.$("#header-avatar"); if (!avatar) { await delay(5000); avatar = await page.$("#header-avatar"); } if (!avatar) { throw new Error("抖音登录已过期,请重新登录"); } if (!page.url().includes("content/upload")) { await page.goto(uploadUrl, { waitUntil: "domcontentloaded" }); await delay(3000); } globalThis.__native?.emitProgress?.("正在上传视频…"); await page.waitForSelector('input[type="file"]', { timeout: 60000 }); const fileInputs = await page.$$('input[type="file"]'); if (!fileInputs.length) throw new Error("未找到视频上传控件"); await fileInputs[0].uploadFile(videoPath); await delay(5000); globalThis.__native?.emitProgress?.("等待视频处理…"); const start = Date.now(); let ready = false; while (Date.now() - start < 300000 && !ready) { const titleBox = await page.$('textarea[placeholder*="标题"], input[placeholder*="标题"]'); if (titleBox) { try { const disabled = await page.evaluate((el) => el.disabled, titleBox); if (!disabled) ready = true; } catch { ready = true; } } if (!ready) await delay(2000); } globalThis.__native?.emitProgress?.("正在上传封面…"); try { await clickIfVisible(page, 'button'); const buttons = await page.$$("button"); for (const btn of buttons) { const text = await page.evaluate((el) => el.textContent || "", btn); if (String(text).includes("选择封面")) { await btn.click(); await delay(2000); break; } } const imgInput = await page.$('input[type="file"][accept*="image"]'); if (imgInput) { await imgInput.uploadFile(coverPath); await delay(3000); } const doneBtns = await page.$$("button"); for (const btn of doneBtns) { const text = await page.evaluate((el) => el.textContent || "", btn); if (String(text).trim() === "完成") { await btn.click(); await delay(2000); break; } } } catch (e) { console.warn("封面上传跳过:", e.message); } globalThis.__native?.emitProgress?.("正在填写标题与话题…"); const titleText = String(title || "").slice(0, 30); await typeIntoFirst( page, [ 'input[placeholder*="填写作品标题"]', 'input[placeholder*="标题"]', 'textarea[placeholder*="标题"]', ], titleText, ); const tagList = Array.isArray(tags) ? tags : []; if (tagList.length) { const descOk = await typeIntoFirst( page, [ 'textarea[placeholder*="描述"]', 'textarea[placeholder*="简介"]', 'motion[contenteditable="true"]', 'div[contenteditable="true"]', ], "", ); if (descOk) { for (const tag of tagList) { const t = String(tag).trim(); if (!t) continue; const clean = t.startsWith("#") ? t : `#${t}`; await page.keyboard.type(clean, { delay: 40 }); await page.keyboard.press("Space"); await delay(400); } } } if (!autoPublish) { return { success: false, status: "manual_pending", message: "内容已填写,请在浏览器中检查并手动点击发布", videoUrl: page.url(), keepPageOpen: true, }; } globalThis.__native?.emitProgress?.("正在点击发布…"); const clicked = await clickPublishButton(page); if (!clicked) { return { success: false, status: "manual_pending", message: "未找到发布按钮,请手动发布", videoUrl: page.url(), keepPageOpen: true, }; } await delay(5000); return { success: true, status: "success", message: "已提交发布", videoUrl: page.url(), }; } module.exports = { publishToDouyin };