// npm 패키지
@openclaw-cn/toutiao-ops
今日头条创作者平台运营自动化 CLI — 支持多账号管理、文章/视频/微头条发布、评论管理、数据分析、创作灵感获取
버전
5
메인테이너
1
라이선스
MIT
최초 publish
2026-03-31
publisher
jiulingyun
tarball
95,462 B
AUTO-PUBLISHED·1개 버전 인덱싱됨·최근 publish: 2026-04-08
// publisher 캠페인by jiulingyun
이 계정에서 catch된 패키지 6건고립된 catch가 아닙니다. 동일 publisher가 5개의 다른 패키지를 추가로 발행했고, 모두 파이프라인이 catch했습니다 — 일회성이 아닌 조직적 캠페인의 형태. 아래 링크는 각 형제 catch의 분석으로 이동합니다.
// offending code· @1.1.4· 1 file flagged
llm: benign · 0.85→ 의심 전송지 없음, 원격 실행 형태 없음 — 2 other host(s).
- @1.1.4··AUTO-PUBLISHED·publisher: jiulingyunheuristic 75/100static flags 1llm benign (0.85) via ollamainstall-scripts:postinstallosv-flagged:MAL-2026-3844child-process-spawn
→ 의심 전송지 없음, 원격 실행 형태 없음 — 2 other host(s).
// offending code· 1 file flaggedpatterns: 1
--- install scripts --- ### postinstall npx playwright install chromium || true --- package/src/inspiration.js (excerpt) --- import { launchBrowser, closeBrowser, sleep, browserFetch, waitForStable, dismissOverlays } from './browser.js'; import { ensureLoggedIn } from './auth-guard.js'; const URLS = { activity: 'https://mp.toutiao.com/profile_v4/activity/task-list', hotspot: 'https://mp.toutiao.com/profile_v4/activity/hot-spot', }; /** * 获取创作灵感列表。 * --type activity(创作活动,默认)| hotspot(热点推荐) */ export async function listInspiration(opts) { const type = opts.type || 'activity'; const targetUrl = URLS[type] || URLS.activity; const { context, page } = await launchBrowser(opts); try { await ensureLoggedIn(page); const apiResponses = []; page.on('response', async (response) => { const url = response.url(); if (url.includes('activity') || url.includes('task') || url.includes('inspiration') || url.includes('hot') || url.includes('topic')) { try { const contentType = response.headers()['content-type'] || ''; if (contentType.includes('json')) { const json = await response.json(); apiResponses.push({ url, data: json }); } } catch {} } }); await page.goto(targetUrl, { waitUntil: 'domcontentloaded', timeout: 30000 }); await waitForStable(page); await sleep(2000, 4000); await dismissOverlays(page); if (apiResponses.length > 0) { return { success: true, type, source: 'api_intercept', data: apiResponses.map(r => r.data), apiCoun --- bundled output (OSV-MAL flagged — LLM scope expansion) --- --- src/comment-manage.js (bundled) --- import { launchBrowser, closeBrowser, sleep, waitForStable, dismissOverlays } from './browser.js'; import { ensureLoggedIn } from './auth-guard.js'; const COMMENT_PAGE = 'https://mp.toutiao.com/profile_v4/manage/comment/all'; /** * 获取评论列表(含子评论/回复)。 * 优先 API 拦截;API 数据中已包含 reply_count。 * 若指定 --with-replies,会逐条点击评论从右侧面板提取子评论。 */ export async function listComments(opts) { const { context, page } = await launchBrowser(opts); try { await ensureLoggedIn(page); const apiData = []; page.on('response', async (response) => { try { const url = response.url(); const ct = response.headers()['content-type'] || ''; if (!ct.includes('json')) return; const json = await response.json(); if (url.includes('comment') || json?.data?.comments || json?.data?.comment_list) { apiData.push(json); } } catch {} }); await page.goto(COMMENT_PAGE, { waitUntil: 'domcontentloaded', timeout: 30000 }); await waitForStable(page); await sleep(3000, 4000); await dismissOverlays(page); // 优先使用 API 数据 const apiComments = apiData.flatMap(d => d?.data || []).filter(c => c.id_str); if (apiComments.length > 0) { const comments = apiComments.map(formatApiComment); // 如果需要获取子评论 if (opts.withReplies) { for (let i = 0; i < comments.length; i++) { if (comments[i].replyCount > 0) { comments[i].replies = await extractRepliesForComment(page, i); } } } return { success: true, source: 'api_intercept', comments, count: comments.length }; } // DOM 回退 const comments = await extractCommentsFromDOM(page); return { success: true, source: 'dom_scrape', comments, count: comments.length }; } finally { await closeBrowser(context); } } /** * 点赞评论。 * 通过 commentId(精确 ID)或评论内容片段定位评论,点击「赞」按钮。 */ export async function likeComment(opts) { const { context, page } = await launchBrowser(opts); try { a --- src/content-manage.js (bundled) --- import { launchBrowser, closeBrowser, sleep, browserFetch, waitForStable, dismissOverlays } from './browser.js'; import { ensureLoggedIn } from './auth-guard.js'; const CONTENT_PAGE = 'https://mp.toutiao.com/profile_v4/manage/content/all'; const TYPE_MAP = { all: '', article: 'article', video: 'video', weitoutiao: 'weitoutiao', }; const STATUS_MAP = { published: 'published', reviewing: 'reviewing', rejected: 'rejected', }; /** * 获取作品列表。 * 导航到内容管理页后,通过拦截 + 浏览器内 fetch 获取结构化数据。 */ export async function listContent(opts) { const { context, page } = await launchBrowser(opts); try { await ensureLoggedIn(page); const apiResponses = []; page.on('response', async (response) => { const url = response.url(); if (url.includes('/pgc/ma/') && url.includes('content') || url.includes('/api/') && url.includes('article')) { try { const json = await response.json(); apiResponses.push({ url, data: json }); } catch {} } }); await page.goto(CONTENT_PAGE, { waitUntil: 'domcontentloaded', timeout: 30000 }); await waitForStable(page); await sleep(2000, 3000); await dismissOverlays(page); if (apiResponses.length > 0) { return { success: true, source: 'api_intercept', items: apiResponses.map(r => r.data), count: apiResponses.length, }; } // 回退:从 DOM 提取 const items = await page.evaluate(() => { const rows = document.querySelectorAll('[class*="content-item"], [class*="article-item"], table tbody tr, [class*="list"] > div[class*="item"]'); return Array.from(rows).map(row => { const title = row.querySelector('[class*="title"] a, [class*="title"] span, td:first-child a')?.textContent?.trim() || ''; const status = row.querySelector('[class*="status"], [class*="state"]')?.textContent?.trim() || ''; const reads = row.querySelector('[class*="read"], [class*="view"]')?.textContent?.trim() || ''; --- src/publish-article.js (bundled) --- import { readFileSync } from 'fs'; import { marked } from 'marked'; import { launchBrowser, closeBrowser, sleep, waitForStable, dismissOverlays } from './browser.js'; import { ensureLoggedIn } from './auth-guard.js'; const PUBLISH_URL = 'https://mp.toutiao.com/profile_v4/graphic/publish'; const TITLE_MAX_LEN = 30; const TITLE_MIN_LEN = 2; /** * 发布图文文章。 * 参数: * --title 文章标题(必填) * --content 正文文本 * --content-file 从文件读取正文 * --cover 封面图片路径(必填,单图模式) * --cover-mode 封面模式: single / triple / none(默认 single) * --first-publish 勾选"头条首发" * --collection 添加至合集名称 * --no-weitoutiao 取消"同时发布微头条" * --declaration 作品声明,逗号分隔 * --draft 存草稿 */ export async function publishArticle(opts) { const { context, page } = await launchBrowser(opts); try { await ensureLoggedIn(page); await page.goto(PUBLISH_URL, { waitUntil: 'domcontentloaded', timeout: 30000 }); await waitForStable(page); await sleep(1500, 2500); await dismissOverlays(page); // ── 标题(2~30 字) ── let title = opts.title; if (title.length < TITLE_MIN_LEN) { throw new Error(`标题过短:至少 ${TITLE_MIN_LEN} 个字,当前 ${title.length} 个字`); } if (title.length > TITLE_MAX_LEN) { title = title.slice(0, TITLE_MAX_LEN); process.stderr.write(`[warn] 标题超过 ${TITLE_MAX_LEN} 字限制,已自动截断为:${title}\n`); } const titleSelector = 'textarea[placeholder*="标题"], input[placeholder*="标题"], [class*="title"] textarea, [class*="title"] input'; await page.waitForSelector(titleSelector, { timeout: 15000 }); await sleep(300, 600); await page.click(titleSelector, { force: true }); await page.keyboard.type(title, { delay: 50 + Math.random() * 80 }); await sleep(500, 1000); // ── 正文 ── let content = opts.content || ''; if (opts.contentFile) { content = readFileSync(opts.contentFile, 'utf-8'); } content = content.replace(/\\n/g, '\n'); if (content) { const editorSelector = '[ --- src/publish-weitoutiao.js (bundled) --- import { launchBrowser, closeBrowser, sleep, waitForStable, dismissOverlays } from './browser.js'; import { ensureLoggedIn } from './auth-guard.js'; const PUBLISH_URL = 'https://mp.toutiao.com/profile_v4/weitoutiao/publish'; /** * 发布微头条。 * 参数: * --content 微头条正文(必填) * --images 图片路径,逗号分隔 * --topic 话题名称(不含 #) * --first-publish 勾选"头条首发" * --declaration 作品声明,逗号分隔,可选值: 取材网络,引用站内,个人观点,引用AI,虚构演绎,投资观点,健康医疗 * --draft 存草稿而非发布 */ export async function publishWeitoutiao(opts) { const { context, page } = await launchBrowser(opts); try { await ensureLoggedIn(page); await page.goto(PUBLISH_URL, { waitUntil: 'domcontentloaded', timeout: 30000 }); await waitForStable(page); await sleep(1500, 2500); await dismissOverlays(page); // ── 输入内容 ── const editorSelector = [ '[contenteditable="true"]', '[class*="editor"] [contenteditable]', 'textarea', ].join(', '); await page.waitForSelector(editorSelector, { timeout: 15000 }); await sleep(300, 600); await page.click(editorSelector, { force: true }); await sleep(200, 400); // 将字面量 \n 转换为真正的换行符 const text = opts.content.replace(/\\n/g, '\n'); const paragraphs = text.split('\n'); for (let i = 0; i < paragraphs.length; i++) { const para = paragraphs[i]; if (para) { await page.keyboard.type(para, { delay: 40 + Math.random() * 80 }); } if (i < paragraphs.length - 1) { await page.keyboard.press('Enter'); await sleep(100, 300); } } await sleep(500, 1000); // ── 图片上传 ── if (opts.images) { const imagePaths = opts.images.split(',').map(p => p.trim()).filter(Boolean); if (imagePaths.length > 0) { await uploadImages(page, imagePaths); } } await sleep(500,
