"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; } } 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 publishBtns = await page.$$("button"); let clicked = false; for (const btn of publishBtns) { const text = (await page.evaluate((el) => el.textContent || "", btn)).trim(); if (text === "发布" || text === "确定发布") { await btn.click(); clicked = true; break; } } if (!clicked) { return { success: false, status: "manual_pending", message: "未找到发布按钮,请手动发布", videoUrl: page.url(), }; } await delay(5000); return { success: true, status: "success", message: "已提交发布", videoUrl: page.url(), }; } module.exports = { publishToDouyin };