Files
yaoyaoai/scripts/nodejs/publish_to_douyin.js
fengchuanhn@gmail.com 7f57dfa9ed 11
2026-05-21 00:19:11 +08:00

198 lines
5.5 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use strict";
/**
* 抖音创作者中心发布(对齐 Electron publishToDouyinPuppeteer 版)
*/
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 };