Files
wehub-resource-sync 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
chore: import upstream snapshot with attribution
2026-07-13 12:39:48 +08:00

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} 删除后仍在作品列表中,删除未确认`);
},
});