9b395f5cc3
E2E Headed Chrome / e2e-headed (macos-15) (push) Has been cancelled
E2E Headed Chrome / e2e-headed (ubuntu-latest) (push) Has been cancelled
E2E Headed Chrome / e2e-headed (windows-latest) (push) Has been cancelled
CI / build (macos-latest) (push) Has been cancelled
CI / build (ubuntu-latest) (push) Has been cancelled
CI / build (windows-latest) (push) Has been cancelled
CI / unit-test (push) Has been cancelled
CI / bun-test (push) Has been cancelled
CI / adapter-test (push) Has been cancelled
CI / smoke-test (macos-latest) (push) Has been cancelled
CI / smoke-test (ubuntu-latest) (push) Has been cancelled
Security Audit / audit (push) Has been cancelled
Build Chrome Extension / build (push) Has been cancelled
Trigger Website Rebuild (Docs Updated) / dispatch (push) Has been cancelled
153 lines
7.2 KiB
JavaScript
153 lines
7.2 KiB
JavaScript
import { cli, Strategy } from '@jackwener/opencli/registry';
|
|
import { ArgumentError, CommandExecutionError } from '@jackwener/opencli/errors';
|
|
import { browserFetch } from './_shared/browser-fetch.js';
|
|
import { requireObjectEvaluateResult } from './_shared/evaluate-result.js';
|
|
|
|
const CREATOR_MANAGE_URL = 'https://creator.douyin.com/creator-micro/content/manage';
|
|
const WORK_LIST_URL = '/janus/douyin/creator/pc/work_list?status=0&count=20&max_cursor=0&scene=star_atlas&device_platform=android&aid=1128';
|
|
|
|
function readAwemeId(raw) {
|
|
const value = String(raw ?? '').trim();
|
|
if (!value) {
|
|
throw new ArgumentError('douyin delete aweme_id cannot be empty');
|
|
}
|
|
if (!/^\d+$/.test(value)) {
|
|
throw new ArgumentError('douyin delete aweme_id must be a numeric id');
|
|
}
|
|
return value;
|
|
}
|
|
|
|
function sleep(ms) {
|
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
}
|
|
|
|
async function deleteViaCreatorManage(page, workId) {
|
|
await page.goto(CREATOR_MANAGE_URL);
|
|
await sleep(3000);
|
|
await sleep(3000);
|
|
const result = requireObjectEvaluateResult(await page.evaluate(`
|
|
(async () => {
|
|
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
const targetId = ${JSON.stringify(String(workId))};
|
|
const textOf = (node) => (node && (node.innerText || node.textContent) || '').trim();
|
|
const normalize = (value) => String(value || '').replace(/\\s+/g, ' ').trim();
|
|
|
|
async function loadTarget() {
|
|
const res = await fetch(${JSON.stringify(WORK_LIST_URL)}, { credentials: 'include' });
|
|
const payload = await res.json();
|
|
const list = Array.isArray(payload.aweme_list) ? payload.aweme_list : [];
|
|
const matches = list
|
|
.map((entry, index) => ({ entry, index }))
|
|
.filter(({ entry }) => String(entry.aweme_id || '') === targetId || String(entry.item_id || '') === targetId);
|
|
if (matches.length === 0) {
|
|
return { ok: false, reason: 'not_found', status_code: payload.status_code, count: list.length };
|
|
}
|
|
if (matches.length !== 1) {
|
|
return { ok: false, reason: 'target_not_unique', count: matches.length };
|
|
}
|
|
const { entry: item, index } = matches[0];
|
|
const title = normalize(item.desc || item.caption || item.title || item.item_title || '');
|
|
return { ok: true, item, index, listCount: list.length, title };
|
|
}
|
|
|
|
function visibleWorkCards() {
|
|
const candidates = Array.from(document.querySelectorAll('[class*="video-card"]'))
|
|
.filter((element) => {
|
|
const text = normalize(textOf(element));
|
|
return text.includes('删除作品') && text.includes('继续编辑');
|
|
});
|
|
return candidates.filter((candidate) => !candidates.some((other) => other !== candidate && other.contains(candidate)));
|
|
}
|
|
|
|
const target = await loadTarget();
|
|
if (!target.ok) return target;
|
|
|
|
const allTab = Array.from(document.querySelectorAll('button,[role="button"],span,div'))
|
|
.find((element) => /^全部作品$/.test(normalize(textOf(element))));
|
|
allTab?.click();
|
|
await sleep(1000);
|
|
for (let attempt = 0; attempt < 20; attempt += 1) {
|
|
const cards = visibleWorkCards();
|
|
if (cards.length >= target.listCount && cards[target.index]) {
|
|
const card = cards[target.index];
|
|
const deleteButton = Array.from(card.querySelectorAll('button,[role="button"],span,div'))
|
|
.find((element) => /^删除作品$/.test(normalize(textOf(element))));
|
|
if (!deleteButton) return { ok: false, reason: 'delete_button_not_found', aweme_id: target.item.aweme_id, item_id: target.item.item_id, index: target.index, cardCount: cards.length };
|
|
deleteButton.click();
|
|
await sleep(800);
|
|
const confirmButton = Array.from(document.querySelectorAll('button,[role="button"]'))
|
|
.find((element) => ['确定', '确认', '删除'].includes(normalize(textOf(element))));
|
|
if (!confirmButton) return { ok: false, reason: 'confirm_button_not_found', aweme_id: target.item.aweme_id, item_id: target.item.item_id };
|
|
confirmButton.click();
|
|
for (let wait = 0; wait < 20; wait += 1) {
|
|
await sleep(500);
|
|
const after = await loadTarget();
|
|
if (!after.ok && after.reason === 'not_found') {
|
|
return { ok: true, aweme_id: target.item.aweme_id, item_id: target.item.item_id, title: target.title };
|
|
}
|
|
}
|
|
return { ok: false, reason: 'delete_not_confirmed', aweme_id: target.item.aweme_id, item_id: target.item.item_id };
|
|
}
|
|
await sleep(500);
|
|
}
|
|
return { ok: false, reason: 'card_not_found', aweme_id: target.item.aweme_id, item_id: target.item.item_id, index: target.index, listCount: target.listCount };
|
|
})()
|
|
`), '抖音后台管理删除响应异常');
|
|
|
|
if (!result?.ok) {
|
|
throw new CommandExecutionError(`抖音后台管理删除失败: ${JSON.stringify(result)}`);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
async function findWorkListItem(page, workId) {
|
|
const data = await browserFetch(page, 'GET', `https://creator.douyin.com${WORK_LIST_URL}`, { timeoutMs: 8000 });
|
|
const list = data.data?.work_list ?? data.aweme_list ?? data.work_list ?? [];
|
|
if (!Array.isArray(list)) {
|
|
throw new CommandExecutionError('抖音作品列表响应缺少 work_list/aweme_list');
|
|
}
|
|
return list.find((entry) => String(entry.aweme_id || '') === workId || String(entry.item_id || '') === workId) || null;
|
|
}
|
|
|
|
cli({
|
|
site: 'douyin',
|
|
name: 'delete',
|
|
access: 'write',
|
|
description: '删除作品(优先使用创作者后台作品管理;找不到时回退到旧删除接口)',
|
|
domain: 'creator.douyin.com',
|
|
strategy: Strategy.COOKIE,
|
|
siteSession: 'persistent',
|
|
args: [
|
|
{ name: 'aweme_id', required: true, positional: true, help: '作品 ID / item_id' },
|
|
],
|
|
columns: ['status'],
|
|
func: async (page, kwargs) => {
|
|
const awemeId = readAwemeId(kwargs.aweme_id);
|
|
try {
|
|
const deleted = await deleteViaCreatorManage(page, awemeId);
|
|
return [{ status: `✅ 已通过后台管理删除 ${deleted.aweme_id || awemeId}` }];
|
|
} catch (fallbackError) {
|
|
const fallbackMessage = fallbackError instanceof Error ? fallbackError.message : String(fallbackError);
|
|
if (!fallbackMessage.includes('"reason":"not_found"')) {
|
|
throw fallbackError;
|
|
}
|
|
}
|
|
|
|
const before = await findWorkListItem(page, awemeId);
|
|
if (!before) {
|
|
throw new CommandExecutionError(`抖音作品 ${awemeId} 未在作品列表中找到,未执行删除`);
|
|
}
|
|
const url = 'https://creator.douyin.com/web/api/media/aweme/delete/?aid=1128';
|
|
await browserFetch(page, 'POST', url, { body: { aweme_id: awemeId }, timeoutMs: 8000 });
|
|
const deadline = Date.now() + 10_000;
|
|
while (Date.now() < deadline) {
|
|
await sleep(500);
|
|
const after = await findWorkListItem(page, awemeId);
|
|
if (!after) {
|
|
return [{ status: `✅ 已删除 ${awemeId}` }];
|
|
}
|
|
}
|
|
throw new CommandExecutionError(`抖音作品 ${awemeId} 删除后仍在作品列表中,删除未确认`);
|
|
},
|
|
});
|