From 7443e12f66e6ae2481d43bb626f288c4c113457a Mon Sep 17 00:00:00 2001 From: tanglong <842690096@qq.com> Date: Thu, 18 Jun 2026 14:46:49 +0800 Subject: [PATCH] xiaohongshu --- CLIENT_README.txt | 2 +- dist/agent-foundation-client-yuedong.zip | Bin 8994 -> 16521 bytes .../CLIENT_README.txt | 2 +- .../package.json | 9 +- .../scripts/local_douyin_worker.js | 26 +- .../scripts/local_media_matrix_worker.js | 217 ++++++++ .../scripts/local_xiaohongshu_worker.js | 517 ++++++++++++++++++ .../scripts/start_local_client.ps1 | 67 ++- .../start_yuedong_client.bat | 2 +- package.json | 9 +- scripts/local_douyin_worker.js | 26 +- scripts/local_media_matrix_worker.js | 217 ++++++++ scripts/local_xiaohongshu_worker.js | 517 ++++++++++++++++++ scripts/start_local_client.ps1 | 67 ++- .../media/model/MediaTask.java | 160 ++++-- start_yuedong_client.bat | 2 +- 16 files changed, 1760 insertions(+), 80 deletions(-) create mode 100644 dist/agent-foundation-client-yuedong/scripts/local_media_matrix_worker.js create mode 100644 dist/agent-foundation-client-yuedong/scripts/local_xiaohongshu_worker.js create mode 100644 scripts/local_media_matrix_worker.js create mode 100644 scripts/local_xiaohongshu_worker.js diff --git a/CLIENT_README.txt b/CLIENT_README.txt index 95842fb..eb8c6e9 100644 --- a/CLIENT_README.txt +++ b/CLIENT_README.txt @@ -7,7 +7,7 @@ Before first use, install: How to start: 1. Unzip this package to a normal folder, for example Desktop\agent-foundation-client. 2. Double-click start_yuedong_client.bat. -3. If a Douyin login page opens, login in the browser, then press Enter in the terminal window. +3. If a Douyin or Xiaohongshu login page opens, login in the browser, then press Enter in the terminal window. 4. Keep the worker terminal window open while using the system. Server API: diff --git a/dist/agent-foundation-client-yuedong.zip b/dist/agent-foundation-client-yuedong.zip index 430b563d004d3b07ae6eb1ff8272776f479f6e2c..a2e52e18b70c8fdc2a96038513a1be5e5882e9d3 100644 GIT binary patch delta 15123 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 delta 7699 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 ''), @@ -418,6 +420,7 @@ async function handleTask(task) { errorMessage: status.loggedIn ? null : status.message, result: { action, + platform: 'douyin', loggedIn: status.loggedIn, message: status.message, url: status.url, @@ -501,7 +504,18 @@ async function main() { throw new Error('Usage: node scripts/local_douyin_worker.js [accountId]'); } -main().catch(error => { - console.error(error.stack || error.message); - process.exit(1); -}); +module.exports = { + checkLogin, + collectMetrics, + login, + profileDir, + workerLoop, + workerOnce +}; + +if (require.main === module) { + main().catch(error => { + console.error(error.stack || error.message); + process.exit(1); + }); +} diff --git a/dist/agent-foundation-client-yuedong/scripts/local_media_matrix_worker.js b/dist/agent-foundation-client-yuedong/scripts/local_media_matrix_worker.js new file mode 100644 index 0000000..9444102 --- /dev/null +++ b/dist/agent-foundation-client-yuedong/scripts/local_media_matrix_worker.js @@ -0,0 +1,217 @@ +const douyin = require('./local_douyin_worker'); +const xiaohongshu = require('./local_xiaohongshu_worker'); + +const DEFAULT_BASE_URL = process.env.MEDIA_BASE_URL || 'http://localhost:8880'; +const DEFAULT_WORKER_ID = process.env.MEDIA_WORKER_ID || 'local-media-matrix-worker'; +const DEFAULT_WORKER_TOKEN = process.env.MEDIA_WORKER_TOKEN || 'local-worker-dev-token'; +const DEFAULT_DOUYIN_ACCOUNT_ID = process.env.DOUYIN_ACCOUNT_ID || 'douyin_company'; +const DEFAULT_XHS_ACCOUNT_ID = process.env.XHS_ACCOUNT_ID || 'xiaohongshu_company'; + +const PLATFORM_HANDLERS = { + douyin: { + accountId: DEFAULT_DOUYIN_ACCOUNT_ID, + collectMetrics: douyin.collectMetrics, + checkLogin: douyin.checkLogin + }, + xiaohongshu: { + accountId: DEFAULT_XHS_ACCOUNT_ID, + collectMetrics: xiaohongshu.collectMetrics, + checkLogin: xiaohongshu.checkLogin + } +}; + +function normalizePlatform(platform) { + const value = String(platform || '').trim().toLowerCase(); + if (value === 'xhs' || value === 'rednote') { + return 'xiaohongshu'; + } + return value; +} + +function requestedPlatforms(task) { + const payload = task.payload || {}; + const raw = Array.isArray(payload.platforms) && payload.platforms.length > 0 + ? payload.platforms + : ['douyin', 'xiaohongshu']; + return raw.map(normalizePlatform).filter(platform => PLATFORM_HANDLERS[platform]); +} + +function accountIdFor(task, platform) { + const payload = task.payload || {}; + const accountIds = payload.accountIds || {}; + if (accountIds && typeof accountIds === 'object' && accountIds[platform]) { + return accountIds[platform]; + } + return PLATFORM_HANDLERS[platform].accountId; +} + +function payloadForPlatform(task, platform) { + const payload = task.payload || {}; + const platformPayloads = payload.platformPayloads || {}; + return { + ...payload, + ...(platformPayloads[platform] || {}), + platforms: undefined, + accountIds: undefined, + platformPayloads: undefined + }; +} + +async function pollTasks() { + const response = await fetch(`${DEFAULT_BASE_URL}/v1/media/workers/poll`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-Worker-Token': DEFAULT_WORKER_TOKEN + }, + body: JSON.stringify({ + workerId: DEFAULT_WORKER_ID, + platforms: ['multi', 'matrix', 'all'], + maxTasks: 3 + }) + }); + if (!response.ok) { + throw new Error(`Poll failed: ${response.status} ${await response.text()}`); + } + return response.json(); +} + +async function submitResult(taskId, body) { + const response = await fetch(`${DEFAULT_BASE_URL}/v1/media/workers/tasks/${taskId}/result`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-Worker-Token': DEFAULT_WORKER_TOKEN + }, + body: JSON.stringify(body) + }); + if (!response.ok) { + throw new Error(`Submit result failed: ${response.status} ${await response.text()}`); + } + return response.json(); +} + +async function handleTask(task) { + const platforms = requestedPlatforms(task); + if (platforms.length === 0) { + return submitResult(task.id, { + status: 'FAILED', + errorMessage: 'No supported platforms requested.', + result: { action: 'sync_metrics', platform: 'multi', platformResults: {} } + }); + } + + const platformResults = {}; + const videos = []; + const metrics = emptyMetrics(); + const errors = []; + + for (const platform of platforms) { + const handler = PLATFORM_HANDLERS[platform]; + const platformTask = { + ...task, + platform, + accountId: accountIdFor(task, platform), + payload: payloadForPlatform(task, platform) + }; + try { + const response = await handler.collectMetrics(platformTask); + const result = response.result || {}; + platformResults[platform] = { + ...result, + platform, + accountId: platformTask.accountId, + status: response.status + }; + addMetrics(metrics, result.metrics || {}); + for (const video of result.videos || []) { + videos.push({ ...video, platform, accountId: platformTask.accountId }); + } + if (response.status !== 'SUCCESS') { + errors.push(`${platform}: ${response.errorMessage || 'failed'}`); + } + } catch (error) { + platformResults[platform] = { + platform, + accountId: platformTask.accountId, + status: 'FAILED', + errorMessage: error.message + }; + errors.push(`${platform}: ${error.message}`); + } + } + + return submitResult(task.id, { + status: errors.length === platforms.length ? 'FAILED' : 'SUCCESS', + errorMessage: errors.length > 0 ? errors.join('; ') : null, + result: { + action: 'sync_metrics', + platform: 'multi', + platforms, + metrics, + videos, + videoCount: videos.length, + platformResults, + extractionMode: 'multi_platform' + } + }); +} + +function emptyMetrics() { + return { views: 0, likes: 0, comments: 0, collects: 0, shares: 0 }; +} + +function addMetrics(target, source) { + target.views += Number(source.views || 0); + target.likes += Number(source.likes || 0); + target.comments += Number(source.comments || 0); + target.collects += Number(source.collects || 0); + target.shares += Number(source.shares || 0); +} + +async function workerOnce() { + const tasks = await pollTasks(); + if (!Array.isArray(tasks) || tasks.length === 0) { + console.log('No pending media matrix tasks.'); + return; + } + for (const task of tasks) { + const result = await handleTask(task); + console.log(`Completed ${task.id}: ${result.status}`); + } +} + +function sleep(ms) { + return new Promise(resolve => setTimeout(resolve, ms)); +} + +async function workerLoop() { + const intervalMs = Number(process.env.MEDIA_WORKER_INTERVAL_MS || 30000); + console.log(`Media matrix worker loop started. baseUrl=${DEFAULT_BASE_URL}, intervalMs=${intervalMs}`); + while (true) { + try { + await workerOnce(); + } catch (error) { + console.error(error.stack || error.message); + } + await sleep(intervalMs); + } +} + +async function main() { + const mode = process.argv[2]; + if (mode === 'worker') { + await workerOnce(); + return; + } + if (mode === 'worker-loop') { + await workerLoop(); + return; + } + throw new Error('Usage: node scripts/local_media_matrix_worker.js '); +} + +main().catch(error => { + console.error(error.stack || error.message); + process.exit(1); +}); diff --git a/dist/agent-foundation-client-yuedong/scripts/local_xiaohongshu_worker.js b/dist/agent-foundation-client-yuedong/scripts/local_xiaohongshu_worker.js new file mode 100644 index 0000000..ab1b07d --- /dev/null +++ b/dist/agent-foundation-client-yuedong/scripts/local_xiaohongshu_worker.js @@ -0,0 +1,517 @@ +const fs = require('fs'); +const path = require('path'); +const readline = require('readline'); + +const DEFAULT_BASE_URL = process.env.MEDIA_BASE_URL || 'http://localhost:8880'; +const DEFAULT_WORKER_ID = process.env.MEDIA_WORKER_ID || 'local-xiaohongshu-worker'; +const DEFAULT_WORKER_TOKEN = process.env.MEDIA_WORKER_TOKEN || 'local-worker-dev-token'; +const DEFAULT_ACCOUNT_ID = process.env.XHS_ACCOUNT_ID || 'xiaohongshu_company'; +const DEFAULT_CREATOR_URL = process.env.XHS_CREATOR_URL || 'https://creator.xiaohongshu.com'; +const DEFAULT_CONTENT_URL = process.env.XHS_CONTENT_URL || 'https://creator.xiaohongshu.com'; +const DEFAULT_PROFILE_ROOT = process.env.XHS_PROFILE_ROOT || path.resolve('data/platform-profiles'); + +const LOGIN_TEXT = new RegExp([ + '\\u626b\\u7801\\u767b\\u5f55', + '\\u9a8c\\u8bc1\\u7801', + '\\u624b\\u673a\\u53f7', + '\\u767b\\u5f55', + '\\u6ce8\\u518c' +].join('|')); + +const CREATOR_TEXT = new RegExp([ + '\\u5c0f\\u7ea2\\u4e66', + '\\u521b\\u4f5c\\u670d\\u52a1', + '\\u7b14\\u8bb0', + '\\u6570\\u636e', + '\\u4f5c\\u54c1', + '\\u53d1\\u5e03' +].join('|')); + +function requirePlaywright() { + try { + return require('playwright'); + } catch (error) { + throw new Error('Playwright is not installed. Run npm.cmd install in the project root first.'); + } +} + +function profileDir(accountId = DEFAULT_ACCOUNT_ID) { + return path.resolve(DEFAULT_PROFILE_ROOT, accountId); +} + +function ensureDir(dir) { + fs.mkdirSync(dir, { recursive: true }); +} + +function browserChannel() { + if (process.env.XHS_BROWSER_CHANNEL) { + return process.env.XHS_BROWSER_CHANNEL; + } + return process.platform === 'win32' ? 'msedge' : undefined; +} + +async function launchContext({ accountId = DEFAULT_ACCOUNT_ID, headless = false } = {}) { + const { chromium } = requirePlaywright(); + const dir = profileDir(accountId); + ensureDir(dir); + const channel = browserChannel(); + const options = { + headless, + viewport: { width: 1440, height: 1000 }, + acceptDownloads: true, + args: [ + '--disable-blink-features=AutomationControlled', + '--disable-dev-shm-usage' + ] + }; + if (channel) { + options.channel = channel; + } + return chromium.launchPersistentContext(dir, options); +} + +async function waitForEnter(message) { + const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); + return new Promise(resolve => { + rl.question(message, () => { + rl.close(); + resolve(); + }); + }); +} + +async function login() { + const accountId = process.argv[3] || DEFAULT_ACCOUNT_ID; + const context = await launchContext({ accountId, headless: false }); + const page = context.pages()[0] || await context.newPage(); + await page.goto(DEFAULT_CREATOR_URL, { waitUntil: 'domcontentloaded', timeout: 60000 }); + console.log(`Xiaohongshu login profile: ${profileDir(accountId)}`); + console.log('Please login in the opened browser. Keep this window open until login succeeds.'); + await waitForEnter('Press Enter here after Xiaohongshu creator center is logged in...'); + const status = await detectLogin(page, accountId); + await context.close(); + console.log(JSON.stringify(status, null, 2)); +} + +async function checkLogin(accountId = DEFAULT_ACCOUNT_ID) { + const context = await launchContext({ accountId, headless: true }); + const page = context.pages()[0] || await context.newPage(); + try { + await page.goto(DEFAULT_CREATOR_URL, { waitUntil: 'domcontentloaded', timeout: 60000 }); + await page.waitForTimeout(2500); + return await detectLogin(page, accountId); + } finally { + await context.close(); + } +} + +async function detectLogin(page, accountId = DEFAULT_ACCOUNT_ID) { + const url = page.url(); + const title = await page.title().catch(() => ''); + const bodyText = await page.locator('body').innerText({ timeout: 5000 }).catch(() => ''); + const loginVisible = LOGIN_TEXT.test(bodyText); + const creatorVisible = CREATOR_TEXT.test(bodyText); + const loggedIn = creatorVisible || (!loginVisible && /xiaohongshu\.com/.test(url)); + return { + loggedIn, + status: loggedIn ? 'SUCCESS' : 'FAILED', + message: loggedIn ? 'Xiaohongshu creator profile is logged in.' : 'Xiaohongshu creator profile is not logged in.', + url, + title, + profileDir: profileDir(accountId) + }; +} + +async function collectMetrics(task) { + const payload = task.payload || {}; + const accountId = task.accountId || DEFAULT_ACCOUNT_ID; + const targetUrl = payload.metricsUrl || payload.contentUrl || DEFAULT_CONTENT_URL; + const headless = payload.headless === false ? false : true; + const context = await launchContext({ accountId, headless }); + const page = context.pages()[0] || await context.newPage(); + try { + await page.goto(targetUrl, { waitUntil: 'domcontentloaded', timeout: 60000 }); + await page.waitForTimeout(Number(payload.waitMs || 6000)); + const status = await detectLogin(page, accountId); + if (!status.loggedIn) { + return { + status: 'FAILED', + errorMessage: status.message, + result: { + action: 'sync_metrics', + platform: 'xiaohongshu', + loggedIn: false, + ...status + } + }; + } + + const text = await page.locator('body').innerText({ timeout: 15000 }).catch(() => ''); + const screenshot = await saveScreenshot(page, task.id); + const videos = await collectVideoMetrics(page, Number(payload.maxVideos || 20)); + const metrics = videos.length > 0 ? summarizeVideos(videos) : extractMetrics(text); + return { + status: 'SUCCESS', + errorMessage: null, + result: { + action: 'sync_metrics', + platform: 'xiaohongshu', + loggedIn: true, + url: page.url(), + title: await page.title().catch(() => ''), + metrics, + videos, + videoCount: videos.length, + extractionMode: videos.length > 0 ? 'video_list' : 'page_summary', + screenshot, + pageTextPreview: compactText(text).slice(0, 2000) + } + }; + } finally { + await context.close().catch(() => {}); + } +} + +async function saveScreenshot(page, taskId) { + const dir = path.resolve('target/media/xiaohongshu'); + ensureDir(dir); + const file = path.join(dir, `${taskId || Date.now()}-metrics.png`); + await page.screenshot({ path: file, fullPage: true }).catch(() => {}); + return file; +} + +function compactText(text) { + return (text || '').replace(/\s+/g, ' ').trim(); +} + +function extractMetrics(text) { + const compact = compactText(text); + return { + views: findMetric(compact, ['\\u6d4f\\u89c8', '\\u89c2\\u770b', '\\u9605\\u8bfb', '\\u66dd\\u5149']), + likes: findMetric(compact, ['\\u70b9\\u8d5e', '\\u8d5e']), + comments: findMetric(compact, ['\\u8bc4\\u8bba']), + collects: findMetric(compact, ['\\u6536\\u85cf']), + shares: findMetric(compact, ['\\u5206\\u4eab', '\\u8f6c\\u53d1']) + }; +} + +async function collectVideoMetrics(page, limit = 20) { + await autoScroll(page); + const blocks = await page.evaluate(maxBlocks => { + const selectors = [ + 'tr', + 'article', + 'li', + '[class*="note"]', + '[class*="card"]', + '[class*="item"]', + '[class*="work"]', + '[class*="video"]', + '[class*="content"]' + ]; + const seen = new Set(); + const rows = []; + for (const selector of selectors) { + for (const element of document.querySelectorAll(selector)) { + if (rows.length >= maxBlocks) { + break; + } + const rect = element.getBoundingClientRect(); + if (rect.width < 140 || rect.height < 40) { + continue; + } + const text = (element.innerText || '').replace(/\s+/g, ' ').trim(); + if (text.length < 10 || text.length > 1200) { + continue; + } + const signature = text.slice(0, 120); + if (seen.has(signature)) { + continue; + } + seen.add(signature); + const link = element.querySelector('a[href]'); + rows.push({ + text, + href: link ? link.href : '' + }); + } + } + return rows; + }, Math.max(limit * 8, 80)); + + const videos = []; + const seen = new Set(); + for (const block of blocks) { + const video = parseVideoBlock(block); + if (!video) { + continue; + } + const key = `${video.title}|${video.views}|${video.likes}|${video.comments}|${video.href}`; + if (seen.has(key)) { + continue; + } + seen.add(key); + videos.push(video); + if (videos.length >= limit) { + break; + } + } + return videos; +} + +async function autoScroll(page) { + await page.evaluate(async () => { + await new Promise(resolve => { + let count = 0; + const timer = setInterval(() => { + window.scrollBy(0, Math.floor(window.innerHeight * 0.8)); + count += 1; + if (count >= 3) { + clearInterval(timer); + resolve(); + } + }, 500); + }); + }).catch(() => {}); + await page.waitForTimeout(800).catch(() => {}); +} + +function parseVideoBlock(block) { + const text = compactText(block.text); + const metrics = extractMetrics(text); + const score = metrics.views + metrics.likes + metrics.comments + metrics.collects + metrics.shares; + if (score <= 0) { + return null; + } + const title = extractTitle(text); + return { + title, + href: block.href || '', + views: metrics.views, + likes: metrics.likes, + comments: metrics.comments, + collects: metrics.collects, + shares: metrics.shares, + interactionCount: metrics.likes + metrics.comments + metrics.collects + metrics.shares, + interactionRate: metrics.views <= 0 + ? 0 + : (metrics.likes + metrics.comments + metrics.collects + metrics.shares) / metrics.views, + rawTextPreview: text.slice(0, 500) + }; +} + +function extractTitle(text) { + const cleaned = text + .replace(/\d{4}[-/.]\d{1,2}[-/.]\d{1,2}.*/g, '') + .replace(/(\u6d4f\u89c8|\u89c2\u770b|\u9605\u8bfb|\u66dd\u5149|\u70b9\u8d5e|\u8d5e|\u8bc4\u8bba|\u6536\u85cf|\u5206\u4eab|\u8f6c\u53d1).*/g, '') + .trim(); + if (cleaned.length >= 2) { + return cleaned.slice(0, 120); + } + return text.slice(0, 80); +} + +function summarizeVideos(videos) { + const summary = { + views: 0, + likes: 0, + comments: 0, + collects: 0, + shares: 0 + }; + for (const video of videos) { + summary.views += Number(video.views || 0); + summary.likes += Number(video.likes || 0); + summary.comments += Number(video.comments || 0); + summary.collects += Number(video.collects || 0); + summary.shares += Number(video.shares || 0); + } + return summary; +} + +function findMetric(text, labels) { + for (const label of labels) { + const labelPattern = new RegExp(label); + const decodedLabel = label.replace(/\\u([0-9a-fA-F]{4})/g, (_, code) => String.fromCharCode(parseInt(code, 16))); + const after = new RegExp(`${label}[^0-9\\d]{0,16}([0-9][0-9,.]*)(\\s*[\\u4e07\\u5343wWkK]?)`); + const before = new RegExp(`([0-9][0-9,.]*)(\\s*[\\u4e07\\u5343wWkK]?)[^0-9\\d]{0,16}${label}`); + if (labelPattern.test(text)) { + const afterMatch = text.match(after); + if (afterMatch) { + return parseMetricNumber(afterMatch[1], afterMatch[2]); + } + const beforeMatch = text.match(before); + if (beforeMatch) { + return parseMetricNumber(beforeMatch[1], beforeMatch[2]); + } + const looseIndex = text.indexOf(decodedLabel); + if (looseIndex >= 0) { + const around = text.slice(Math.max(0, looseIndex - 30), looseIndex + 60); + const number = around.match(/([0-9][0-9,.]*)(\s*[\u4e07\u5343wWkK]?)/); + if (number) { + return parseMetricNumber(number[1], number[2]); + } + } + } + } + return 0; +} + +function parseMetricNumber(raw, unit = '') { + const value = Number(String(raw).replace(/,/g, '')); + if (!Number.isFinite(value)) { + return 0; + } + const normalizedUnit = unit.trim().toLowerCase(); + if (normalizedUnit.includes('\u4e07') || normalizedUnit.includes('w')) { + return Math.round(value * 10000); + } + if (normalizedUnit.includes('\u5343') || normalizedUnit.includes('k')) { + return Math.round(value * 1000); + } + return Math.round(value); +} + +async function pollTasks() { + const response = await fetch(`${DEFAULT_BASE_URL}/v1/media/workers/poll`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-Worker-Token': DEFAULT_WORKER_TOKEN + }, + body: JSON.stringify({ + workerId: DEFAULT_WORKER_ID, + platforms: ['xiaohongshu'], + maxTasks: 5 + }) + }); + if (!response.ok) { + throw new Error(`Poll failed: ${response.status} ${await response.text()}`); + } + return response.json(); +} + +async function submitResult(taskId, body) { + const response = await fetch(`${DEFAULT_BASE_URL}/v1/media/workers/tasks/${taskId}/result`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-Worker-Token': DEFAULT_WORKER_TOKEN + }, + body: JSON.stringify(body) + }); + if (!response.ok) { + throw new Error(`Submit result failed: ${response.status} ${await response.text()}`); + } + return response.json(); +} + +async function handleTask(task) { + const payload = task.payload || {}; + const action = payload.action || (task.taskType === 'SYNC_METRICS' ? 'sync_metrics' : 'check_login'); + if (action === 'check_login') { + const status = await checkLogin(task.accountId || DEFAULT_ACCOUNT_ID); + return submitResult(task.id, { + status: status.loggedIn ? 'SUCCESS' : 'FAILED', + errorMessage: status.loggedIn ? null : status.message, + result: { + action, + platform: 'xiaohongshu', + loggedIn: status.loggedIn, + message: status.message, + url: status.url, + title: status.title, + profileDir: profileDir(task.accountId || DEFAULT_ACCOUNT_ID) + } + }); + } + + if (action === 'sync_metrics') { + const result = await collectMetrics(task); + return submitResult(task.id, result); + } + + return submitResult(task.id, { + status: 'FAILED', + errorMessage: `Unsupported local Xiaohongshu worker action: ${action}`, + result: { action, platform: 'xiaohongshu' } + }); +} + +async function workerOnce() { + const tasks = await pollTasks(); + if (!Array.isArray(tasks) || tasks.length === 0) { + console.log('No pending Xiaohongshu tasks.'); + return; + } + for (const task of tasks) { + const result = await handleTask(task); + console.log(`Completed ${task.id}: ${result.status}`); + } +} + +function sleep(ms) { + return new Promise(resolve => setTimeout(resolve, ms)); +} + +async function workerLoop() { + const intervalMs = Number(process.env.MEDIA_WORKER_INTERVAL_MS || 30000); + console.log(`Xiaohongshu worker loop started. baseUrl=${DEFAULT_BASE_URL}, intervalMs=${intervalMs}`); + while (true) { + try { + await workerOnce(); + } catch (error) { + console.error(error.stack || error.message); + } + await sleep(intervalMs); + } +} + +async function main() { + const mode = process.argv[2]; + if (mode === 'login') { + await login(); + return; + } + if (mode === 'check-login') { + const status = await checkLogin(process.argv[3] || DEFAULT_ACCOUNT_ID); + console.log(JSON.stringify(status, null, 2)); + process.exit(status.loggedIn ? 0 : 2); + } + if (mode === 'metrics') { + const task = { + id: `manual_${Date.now()}`, + taskType: 'SYNC_METRICS', + platform: 'xiaohongshu', + accountId: process.argv[3] || DEFAULT_ACCOUNT_ID, + payload: { action: 'sync_metrics', headless: false } + }; + console.log(JSON.stringify((await collectMetrics(task)).result, null, 2)); + return; + } + if (mode === 'worker') { + await workerOnce(); + return; + } + if (mode === 'worker-loop') { + await workerLoop(); + return; + } + throw new Error('Usage: node scripts/local_xiaohongshu_worker.js [accountId]'); +} + +module.exports = { + checkLogin, + collectMetrics, + login, + profileDir, + workerLoop, + workerOnce +}; + +if (require.main === module) { + main().catch(error => { + console.error(error.stack || error.message); + process.exit(1); + }); +} diff --git a/dist/agent-foundation-client-yuedong/scripts/start_local_client.ps1 b/dist/agent-foundation-client-yuedong/scripts/start_local_client.ps1 index d20221f..7c8590f 100644 --- a/dist/agent-foundation-client-yuedong/scripts/start_local_client.ps1 +++ b/dist/agent-foundation-client-yuedong/scripts/start_local_client.ps1 @@ -1,8 +1,10 @@ param( [string]$BaseUrl = "http://localhost:8880", [string]$AccountId = "douyin_company", + [string]$XhsAccountId = "xiaohongshu_company", [int]$WorkerIntervalSeconds = 30, - [switch]$RemoteBackend + [switch]$RemoteBackend, + [string]$Platforms = "douyin,xiaohongshu" ) $ErrorActionPreference = "Stop" @@ -115,25 +117,62 @@ if ($RemoteBackend -or -not (Test-LocalBaseUrl $BaseUrl)) { Write-Step "Checking Douyin login" $env:MEDIA_BASE_URL = $BaseUrl $env:DOUYIN_ACCOUNT_ID = $AccountId -$checkOutput = & npm.cmd run douyin:check -- $AccountId 2>&1 -if ($LASTEXITCODE -ne 0) { - Write-Host $checkOutput - Write-Host "" - Write-Host "Douyin login is required. A browser will open. Login, then press Enter in that window." - & npm.cmd run douyin:login -- $AccountId +$platformList = $Platforms.Split(",") | ForEach-Object { $_.Trim().ToLower() } | Where-Object { $_ } + +if ($platformList -contains "douyin") { + $checkOutput = & npm.cmd run douyin:check -- $AccountId 2>&1 if ($LASTEXITCODE -ne 0) { - throw "Douyin login failed." + Write-Host $checkOutput + Write-Host "" + Write-Host "Douyin login is required. A browser will open. Login, then press Enter in that window." + & npm.cmd run douyin:login -- $AccountId + if ($LASTEXITCODE -ne 0) { + throw "Douyin login failed." + } + } else { + Write-Host "Douyin login profile is ready." } -} else { - Write-Host "Douyin login profile is ready." } -Write-Step "Starting Douyin worker loop" -$workerCommand = "Set-Location '$Root'; `$env:MEDIA_BASE_URL='$BaseUrl'; `$env:DOUYIN_ACCOUNT_ID='$AccountId'; `$env:MEDIA_WORKER_INTERVAL_MS='$($WorkerIntervalSeconds * 1000)'; npm.cmd run douyin:worker:loop" -Start-Process powershell.exe -ArgumentList @("-NoExit", "-ExecutionPolicy", "Bypass", "-Command", $workerCommand) -WindowStyle Normal +if ($platformList -contains "xiaohongshu") { + Write-Step "Checking Xiaohongshu login" + $env:XHS_ACCOUNT_ID = $XhsAccountId + $xhsCheckOutput = & npm.cmd run xhs:check -- $XhsAccountId 2>&1 + if ($LASTEXITCODE -ne 0) { + Write-Host $xhsCheckOutput + Write-Host "" + Write-Host "Xiaohongshu login is required. A browser will open. Login, then press Enter in that window." + & npm.cmd run xhs:login -- $XhsAccountId + if ($LASTEXITCODE -ne 0) { + throw "Xiaohongshu login failed." + } + } else { + Write-Host "Xiaohongshu login profile is ready." + } +} + +if ($platformList -contains "douyin") { + Write-Step "Starting Douyin worker loop" + $workerCommand = "Set-Location '$Root'; `$env:MEDIA_BASE_URL='$BaseUrl'; `$env:DOUYIN_ACCOUNT_ID='$AccountId'; `$env:MEDIA_WORKER_INTERVAL_MS='$($WorkerIntervalSeconds * 1000)'; npm.cmd run douyin:worker:loop" + Start-Process powershell.exe -ArgumentList @("-NoExit", "-ExecutionPolicy", "Bypass", "-Command", $workerCommand) -WindowStyle Normal +} + +if ($platformList -contains "xiaohongshu") { + Write-Step "Starting Xiaohongshu worker loop" + $xhsWorkerCommand = "Set-Location '$Root'; `$env:MEDIA_BASE_URL='$BaseUrl'; `$env:XHS_ACCOUNT_ID='$XhsAccountId'; `$env:MEDIA_WORKER_INTERVAL_MS='$($WorkerIntervalSeconds * 1000)'; npm.cmd run xhs:worker:loop" + Start-Process powershell.exe -ArgumentList @("-NoExit", "-ExecutionPolicy", "Bypass", "-Command", $xhsWorkerCommand) -WindowStyle Normal +} + +if (($platformList -contains "douyin") -and ($platformList -contains "xiaohongshu")) { + Write-Step "Starting media matrix worker loop" + $matrixWorkerCommand = "Set-Location '$Root'; `$env:MEDIA_BASE_URL='$BaseUrl'; `$env:DOUYIN_ACCOUNT_ID='$AccountId'; `$env:XHS_ACCOUNT_ID='$XhsAccountId'; `$env:MEDIA_WORKER_INTERVAL_MS='$($WorkerIntervalSeconds * 1000)'; npm.cmd run media:worker:loop" + Start-Process powershell.exe -ArgumentList @("-NoExit", "-ExecutionPolicy", "Bypass", "-Command", $matrixWorkerCommand) -WindowStyle Normal +} Write-Host "" Write-Host "Local client is ready." Write-Host "Backend: $BaseUrl" -Write-Host "Account: $AccountId" +Write-Host "Douyin account: $AccountId" +Write-Host "Xiaohongshu account: $XhsAccountId" +Write-Host "Platforms: $($platformList -join ', ')" Write-Host "Worker polling interval: $WorkerIntervalSeconds seconds" diff --git a/dist/agent-foundation-client-yuedong/start_yuedong_client.bat b/dist/agent-foundation-client-yuedong/start_yuedong_client.bat index df335ef..863c5a0 100644 --- a/dist/agent-foundation-client-yuedong/start_yuedong_client.bat +++ b/dist/agent-foundation-client-yuedong/start_yuedong_client.bat @@ -1,5 +1,5 @@ @echo off setlocal cd /d "%~dp0" -powershell.exe -NoProfile -ExecutionPolicy Bypass -File "%~dp0scripts\start_local_client.ps1" -BaseUrl "http://agent.yuedongsports.com" -RemoteBackend +powershell.exe -NoProfile -ExecutionPolicy Bypass -File "%~dp0scripts\start_local_client.ps1" -BaseUrl "http://agent.yuedongsports.com" -RemoteBackend -Platforms "douyin,xiaohongshu" pause diff --git a/package.json b/package.json index 97c513e..e81aec7 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,14 @@ "douyin:worker": "node scripts/local_douyin_worker.js worker", "douyin:worker:loop": "node scripts/local_douyin_worker.js worker-loop", "douyin:check": "node scripts/local_douyin_worker.js check-login", - "douyin:metrics": "node scripts/local_douyin_worker.js metrics" + "douyin:metrics": "node scripts/local_douyin_worker.js metrics", + "xhs:login": "node scripts/local_xiaohongshu_worker.js login", + "xhs:worker": "node scripts/local_xiaohongshu_worker.js worker", + "xhs:worker:loop": "node scripts/local_xiaohongshu_worker.js worker-loop", + "xhs:check": "node scripts/local_xiaohongshu_worker.js check-login", + "xhs:metrics": "node scripts/local_xiaohongshu_worker.js metrics", + "media:worker": "node scripts/local_media_matrix_worker.js worker", + "media:worker:loop": "node scripts/local_media_matrix_worker.js worker-loop" }, "dependencies": { "playwright": "^1.61.0" diff --git a/scripts/local_douyin_worker.js b/scripts/local_douyin_worker.js index 9c8c3cf..c4d1a24 100644 --- a/scripts/local_douyin_worker.js +++ b/scripts/local_douyin_worker.js @@ -137,8 +137,9 @@ async function collectMetrics(task) { status: 'FAILED', errorMessage: status.message, result: { - action: 'sync_metrics', - loggedIn: false, + action: 'sync_metrics', + platform: 'douyin', + loggedIn: false, ...status } }; @@ -153,6 +154,7 @@ async function collectMetrics(task) { errorMessage: null, result: { action: 'sync_metrics', + platform: 'douyin', loggedIn: true, url: page.url(), title: await page.title().catch(() => ''), @@ -418,6 +420,7 @@ async function handleTask(task) { errorMessage: status.loggedIn ? null : status.message, result: { action, + platform: 'douyin', loggedIn: status.loggedIn, message: status.message, url: status.url, @@ -501,7 +504,18 @@ async function main() { throw new Error('Usage: node scripts/local_douyin_worker.js [accountId]'); } -main().catch(error => { - console.error(error.stack || error.message); - process.exit(1); -}); +module.exports = { + checkLogin, + collectMetrics, + login, + profileDir, + workerLoop, + workerOnce +}; + +if (require.main === module) { + main().catch(error => { + console.error(error.stack || error.message); + process.exit(1); + }); +} diff --git a/scripts/local_media_matrix_worker.js b/scripts/local_media_matrix_worker.js new file mode 100644 index 0000000..9444102 --- /dev/null +++ b/scripts/local_media_matrix_worker.js @@ -0,0 +1,217 @@ +const douyin = require('./local_douyin_worker'); +const xiaohongshu = require('./local_xiaohongshu_worker'); + +const DEFAULT_BASE_URL = process.env.MEDIA_BASE_URL || 'http://localhost:8880'; +const DEFAULT_WORKER_ID = process.env.MEDIA_WORKER_ID || 'local-media-matrix-worker'; +const DEFAULT_WORKER_TOKEN = process.env.MEDIA_WORKER_TOKEN || 'local-worker-dev-token'; +const DEFAULT_DOUYIN_ACCOUNT_ID = process.env.DOUYIN_ACCOUNT_ID || 'douyin_company'; +const DEFAULT_XHS_ACCOUNT_ID = process.env.XHS_ACCOUNT_ID || 'xiaohongshu_company'; + +const PLATFORM_HANDLERS = { + douyin: { + accountId: DEFAULT_DOUYIN_ACCOUNT_ID, + collectMetrics: douyin.collectMetrics, + checkLogin: douyin.checkLogin + }, + xiaohongshu: { + accountId: DEFAULT_XHS_ACCOUNT_ID, + collectMetrics: xiaohongshu.collectMetrics, + checkLogin: xiaohongshu.checkLogin + } +}; + +function normalizePlatform(platform) { + const value = String(platform || '').trim().toLowerCase(); + if (value === 'xhs' || value === 'rednote') { + return 'xiaohongshu'; + } + return value; +} + +function requestedPlatforms(task) { + const payload = task.payload || {}; + const raw = Array.isArray(payload.platforms) && payload.platforms.length > 0 + ? payload.platforms + : ['douyin', 'xiaohongshu']; + return raw.map(normalizePlatform).filter(platform => PLATFORM_HANDLERS[platform]); +} + +function accountIdFor(task, platform) { + const payload = task.payload || {}; + const accountIds = payload.accountIds || {}; + if (accountIds && typeof accountIds === 'object' && accountIds[platform]) { + return accountIds[platform]; + } + return PLATFORM_HANDLERS[platform].accountId; +} + +function payloadForPlatform(task, platform) { + const payload = task.payload || {}; + const platformPayloads = payload.platformPayloads || {}; + return { + ...payload, + ...(platformPayloads[platform] || {}), + platforms: undefined, + accountIds: undefined, + platformPayloads: undefined + }; +} + +async function pollTasks() { + const response = await fetch(`${DEFAULT_BASE_URL}/v1/media/workers/poll`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-Worker-Token': DEFAULT_WORKER_TOKEN + }, + body: JSON.stringify({ + workerId: DEFAULT_WORKER_ID, + platforms: ['multi', 'matrix', 'all'], + maxTasks: 3 + }) + }); + if (!response.ok) { + throw new Error(`Poll failed: ${response.status} ${await response.text()}`); + } + return response.json(); +} + +async function submitResult(taskId, body) { + const response = await fetch(`${DEFAULT_BASE_URL}/v1/media/workers/tasks/${taskId}/result`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-Worker-Token': DEFAULT_WORKER_TOKEN + }, + body: JSON.stringify(body) + }); + if (!response.ok) { + throw new Error(`Submit result failed: ${response.status} ${await response.text()}`); + } + return response.json(); +} + +async function handleTask(task) { + const platforms = requestedPlatforms(task); + if (platforms.length === 0) { + return submitResult(task.id, { + status: 'FAILED', + errorMessage: 'No supported platforms requested.', + result: { action: 'sync_metrics', platform: 'multi', platformResults: {} } + }); + } + + const platformResults = {}; + const videos = []; + const metrics = emptyMetrics(); + const errors = []; + + for (const platform of platforms) { + const handler = PLATFORM_HANDLERS[platform]; + const platformTask = { + ...task, + platform, + accountId: accountIdFor(task, platform), + payload: payloadForPlatform(task, platform) + }; + try { + const response = await handler.collectMetrics(platformTask); + const result = response.result || {}; + platformResults[platform] = { + ...result, + platform, + accountId: platformTask.accountId, + status: response.status + }; + addMetrics(metrics, result.metrics || {}); + for (const video of result.videos || []) { + videos.push({ ...video, platform, accountId: platformTask.accountId }); + } + if (response.status !== 'SUCCESS') { + errors.push(`${platform}: ${response.errorMessage || 'failed'}`); + } + } catch (error) { + platformResults[platform] = { + platform, + accountId: platformTask.accountId, + status: 'FAILED', + errorMessage: error.message + }; + errors.push(`${platform}: ${error.message}`); + } + } + + return submitResult(task.id, { + status: errors.length === platforms.length ? 'FAILED' : 'SUCCESS', + errorMessage: errors.length > 0 ? errors.join('; ') : null, + result: { + action: 'sync_metrics', + platform: 'multi', + platforms, + metrics, + videos, + videoCount: videos.length, + platformResults, + extractionMode: 'multi_platform' + } + }); +} + +function emptyMetrics() { + return { views: 0, likes: 0, comments: 0, collects: 0, shares: 0 }; +} + +function addMetrics(target, source) { + target.views += Number(source.views || 0); + target.likes += Number(source.likes || 0); + target.comments += Number(source.comments || 0); + target.collects += Number(source.collects || 0); + target.shares += Number(source.shares || 0); +} + +async function workerOnce() { + const tasks = await pollTasks(); + if (!Array.isArray(tasks) || tasks.length === 0) { + console.log('No pending media matrix tasks.'); + return; + } + for (const task of tasks) { + const result = await handleTask(task); + console.log(`Completed ${task.id}: ${result.status}`); + } +} + +function sleep(ms) { + return new Promise(resolve => setTimeout(resolve, ms)); +} + +async function workerLoop() { + const intervalMs = Number(process.env.MEDIA_WORKER_INTERVAL_MS || 30000); + console.log(`Media matrix worker loop started. baseUrl=${DEFAULT_BASE_URL}, intervalMs=${intervalMs}`); + while (true) { + try { + await workerOnce(); + } catch (error) { + console.error(error.stack || error.message); + } + await sleep(intervalMs); + } +} + +async function main() { + const mode = process.argv[2]; + if (mode === 'worker') { + await workerOnce(); + return; + } + if (mode === 'worker-loop') { + await workerLoop(); + return; + } + throw new Error('Usage: node scripts/local_media_matrix_worker.js '); +} + +main().catch(error => { + console.error(error.stack || error.message); + process.exit(1); +}); diff --git a/scripts/local_xiaohongshu_worker.js b/scripts/local_xiaohongshu_worker.js new file mode 100644 index 0000000..ab1b07d --- /dev/null +++ b/scripts/local_xiaohongshu_worker.js @@ -0,0 +1,517 @@ +const fs = require('fs'); +const path = require('path'); +const readline = require('readline'); + +const DEFAULT_BASE_URL = process.env.MEDIA_BASE_URL || 'http://localhost:8880'; +const DEFAULT_WORKER_ID = process.env.MEDIA_WORKER_ID || 'local-xiaohongshu-worker'; +const DEFAULT_WORKER_TOKEN = process.env.MEDIA_WORKER_TOKEN || 'local-worker-dev-token'; +const DEFAULT_ACCOUNT_ID = process.env.XHS_ACCOUNT_ID || 'xiaohongshu_company'; +const DEFAULT_CREATOR_URL = process.env.XHS_CREATOR_URL || 'https://creator.xiaohongshu.com'; +const DEFAULT_CONTENT_URL = process.env.XHS_CONTENT_URL || 'https://creator.xiaohongshu.com'; +const DEFAULT_PROFILE_ROOT = process.env.XHS_PROFILE_ROOT || path.resolve('data/platform-profiles'); + +const LOGIN_TEXT = new RegExp([ + '\\u626b\\u7801\\u767b\\u5f55', + '\\u9a8c\\u8bc1\\u7801', + '\\u624b\\u673a\\u53f7', + '\\u767b\\u5f55', + '\\u6ce8\\u518c' +].join('|')); + +const CREATOR_TEXT = new RegExp([ + '\\u5c0f\\u7ea2\\u4e66', + '\\u521b\\u4f5c\\u670d\\u52a1', + '\\u7b14\\u8bb0', + '\\u6570\\u636e', + '\\u4f5c\\u54c1', + '\\u53d1\\u5e03' +].join('|')); + +function requirePlaywright() { + try { + return require('playwright'); + } catch (error) { + throw new Error('Playwright is not installed. Run npm.cmd install in the project root first.'); + } +} + +function profileDir(accountId = DEFAULT_ACCOUNT_ID) { + return path.resolve(DEFAULT_PROFILE_ROOT, accountId); +} + +function ensureDir(dir) { + fs.mkdirSync(dir, { recursive: true }); +} + +function browserChannel() { + if (process.env.XHS_BROWSER_CHANNEL) { + return process.env.XHS_BROWSER_CHANNEL; + } + return process.platform === 'win32' ? 'msedge' : undefined; +} + +async function launchContext({ accountId = DEFAULT_ACCOUNT_ID, headless = false } = {}) { + const { chromium } = requirePlaywright(); + const dir = profileDir(accountId); + ensureDir(dir); + const channel = browserChannel(); + const options = { + headless, + viewport: { width: 1440, height: 1000 }, + acceptDownloads: true, + args: [ + '--disable-blink-features=AutomationControlled', + '--disable-dev-shm-usage' + ] + }; + if (channel) { + options.channel = channel; + } + return chromium.launchPersistentContext(dir, options); +} + +async function waitForEnter(message) { + const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); + return new Promise(resolve => { + rl.question(message, () => { + rl.close(); + resolve(); + }); + }); +} + +async function login() { + const accountId = process.argv[3] || DEFAULT_ACCOUNT_ID; + const context = await launchContext({ accountId, headless: false }); + const page = context.pages()[0] || await context.newPage(); + await page.goto(DEFAULT_CREATOR_URL, { waitUntil: 'domcontentloaded', timeout: 60000 }); + console.log(`Xiaohongshu login profile: ${profileDir(accountId)}`); + console.log('Please login in the opened browser. Keep this window open until login succeeds.'); + await waitForEnter('Press Enter here after Xiaohongshu creator center is logged in...'); + const status = await detectLogin(page, accountId); + await context.close(); + console.log(JSON.stringify(status, null, 2)); +} + +async function checkLogin(accountId = DEFAULT_ACCOUNT_ID) { + const context = await launchContext({ accountId, headless: true }); + const page = context.pages()[0] || await context.newPage(); + try { + await page.goto(DEFAULT_CREATOR_URL, { waitUntil: 'domcontentloaded', timeout: 60000 }); + await page.waitForTimeout(2500); + return await detectLogin(page, accountId); + } finally { + await context.close(); + } +} + +async function detectLogin(page, accountId = DEFAULT_ACCOUNT_ID) { + const url = page.url(); + const title = await page.title().catch(() => ''); + const bodyText = await page.locator('body').innerText({ timeout: 5000 }).catch(() => ''); + const loginVisible = LOGIN_TEXT.test(bodyText); + const creatorVisible = CREATOR_TEXT.test(bodyText); + const loggedIn = creatorVisible || (!loginVisible && /xiaohongshu\.com/.test(url)); + return { + loggedIn, + status: loggedIn ? 'SUCCESS' : 'FAILED', + message: loggedIn ? 'Xiaohongshu creator profile is logged in.' : 'Xiaohongshu creator profile is not logged in.', + url, + title, + profileDir: profileDir(accountId) + }; +} + +async function collectMetrics(task) { + const payload = task.payload || {}; + const accountId = task.accountId || DEFAULT_ACCOUNT_ID; + const targetUrl = payload.metricsUrl || payload.contentUrl || DEFAULT_CONTENT_URL; + const headless = payload.headless === false ? false : true; + const context = await launchContext({ accountId, headless }); + const page = context.pages()[0] || await context.newPage(); + try { + await page.goto(targetUrl, { waitUntil: 'domcontentloaded', timeout: 60000 }); + await page.waitForTimeout(Number(payload.waitMs || 6000)); + const status = await detectLogin(page, accountId); + if (!status.loggedIn) { + return { + status: 'FAILED', + errorMessage: status.message, + result: { + action: 'sync_metrics', + platform: 'xiaohongshu', + loggedIn: false, + ...status + } + }; + } + + const text = await page.locator('body').innerText({ timeout: 15000 }).catch(() => ''); + const screenshot = await saveScreenshot(page, task.id); + const videos = await collectVideoMetrics(page, Number(payload.maxVideos || 20)); + const metrics = videos.length > 0 ? summarizeVideos(videos) : extractMetrics(text); + return { + status: 'SUCCESS', + errorMessage: null, + result: { + action: 'sync_metrics', + platform: 'xiaohongshu', + loggedIn: true, + url: page.url(), + title: await page.title().catch(() => ''), + metrics, + videos, + videoCount: videos.length, + extractionMode: videos.length > 0 ? 'video_list' : 'page_summary', + screenshot, + pageTextPreview: compactText(text).slice(0, 2000) + } + }; + } finally { + await context.close().catch(() => {}); + } +} + +async function saveScreenshot(page, taskId) { + const dir = path.resolve('target/media/xiaohongshu'); + ensureDir(dir); + const file = path.join(dir, `${taskId || Date.now()}-metrics.png`); + await page.screenshot({ path: file, fullPage: true }).catch(() => {}); + return file; +} + +function compactText(text) { + return (text || '').replace(/\s+/g, ' ').trim(); +} + +function extractMetrics(text) { + const compact = compactText(text); + return { + views: findMetric(compact, ['\\u6d4f\\u89c8', '\\u89c2\\u770b', '\\u9605\\u8bfb', '\\u66dd\\u5149']), + likes: findMetric(compact, ['\\u70b9\\u8d5e', '\\u8d5e']), + comments: findMetric(compact, ['\\u8bc4\\u8bba']), + collects: findMetric(compact, ['\\u6536\\u85cf']), + shares: findMetric(compact, ['\\u5206\\u4eab', '\\u8f6c\\u53d1']) + }; +} + +async function collectVideoMetrics(page, limit = 20) { + await autoScroll(page); + const blocks = await page.evaluate(maxBlocks => { + const selectors = [ + 'tr', + 'article', + 'li', + '[class*="note"]', + '[class*="card"]', + '[class*="item"]', + '[class*="work"]', + '[class*="video"]', + '[class*="content"]' + ]; + const seen = new Set(); + const rows = []; + for (const selector of selectors) { + for (const element of document.querySelectorAll(selector)) { + if (rows.length >= maxBlocks) { + break; + } + const rect = element.getBoundingClientRect(); + if (rect.width < 140 || rect.height < 40) { + continue; + } + const text = (element.innerText || '').replace(/\s+/g, ' ').trim(); + if (text.length < 10 || text.length > 1200) { + continue; + } + const signature = text.slice(0, 120); + if (seen.has(signature)) { + continue; + } + seen.add(signature); + const link = element.querySelector('a[href]'); + rows.push({ + text, + href: link ? link.href : '' + }); + } + } + return rows; + }, Math.max(limit * 8, 80)); + + const videos = []; + const seen = new Set(); + for (const block of blocks) { + const video = parseVideoBlock(block); + if (!video) { + continue; + } + const key = `${video.title}|${video.views}|${video.likes}|${video.comments}|${video.href}`; + if (seen.has(key)) { + continue; + } + seen.add(key); + videos.push(video); + if (videos.length >= limit) { + break; + } + } + return videos; +} + +async function autoScroll(page) { + await page.evaluate(async () => { + await new Promise(resolve => { + let count = 0; + const timer = setInterval(() => { + window.scrollBy(0, Math.floor(window.innerHeight * 0.8)); + count += 1; + if (count >= 3) { + clearInterval(timer); + resolve(); + } + }, 500); + }); + }).catch(() => {}); + await page.waitForTimeout(800).catch(() => {}); +} + +function parseVideoBlock(block) { + const text = compactText(block.text); + const metrics = extractMetrics(text); + const score = metrics.views + metrics.likes + metrics.comments + metrics.collects + metrics.shares; + if (score <= 0) { + return null; + } + const title = extractTitle(text); + return { + title, + href: block.href || '', + views: metrics.views, + likes: metrics.likes, + comments: metrics.comments, + collects: metrics.collects, + shares: metrics.shares, + interactionCount: metrics.likes + metrics.comments + metrics.collects + metrics.shares, + interactionRate: metrics.views <= 0 + ? 0 + : (metrics.likes + metrics.comments + metrics.collects + metrics.shares) / metrics.views, + rawTextPreview: text.slice(0, 500) + }; +} + +function extractTitle(text) { + const cleaned = text + .replace(/\d{4}[-/.]\d{1,2}[-/.]\d{1,2}.*/g, '') + .replace(/(\u6d4f\u89c8|\u89c2\u770b|\u9605\u8bfb|\u66dd\u5149|\u70b9\u8d5e|\u8d5e|\u8bc4\u8bba|\u6536\u85cf|\u5206\u4eab|\u8f6c\u53d1).*/g, '') + .trim(); + if (cleaned.length >= 2) { + return cleaned.slice(0, 120); + } + return text.slice(0, 80); +} + +function summarizeVideos(videos) { + const summary = { + views: 0, + likes: 0, + comments: 0, + collects: 0, + shares: 0 + }; + for (const video of videos) { + summary.views += Number(video.views || 0); + summary.likes += Number(video.likes || 0); + summary.comments += Number(video.comments || 0); + summary.collects += Number(video.collects || 0); + summary.shares += Number(video.shares || 0); + } + return summary; +} + +function findMetric(text, labels) { + for (const label of labels) { + const labelPattern = new RegExp(label); + const decodedLabel = label.replace(/\\u([0-9a-fA-F]{4})/g, (_, code) => String.fromCharCode(parseInt(code, 16))); + const after = new RegExp(`${label}[^0-9\\d]{0,16}([0-9][0-9,.]*)(\\s*[\\u4e07\\u5343wWkK]?)`); + const before = new RegExp(`([0-9][0-9,.]*)(\\s*[\\u4e07\\u5343wWkK]?)[^0-9\\d]{0,16}${label}`); + if (labelPattern.test(text)) { + const afterMatch = text.match(after); + if (afterMatch) { + return parseMetricNumber(afterMatch[1], afterMatch[2]); + } + const beforeMatch = text.match(before); + if (beforeMatch) { + return parseMetricNumber(beforeMatch[1], beforeMatch[2]); + } + const looseIndex = text.indexOf(decodedLabel); + if (looseIndex >= 0) { + const around = text.slice(Math.max(0, looseIndex - 30), looseIndex + 60); + const number = around.match(/([0-9][0-9,.]*)(\s*[\u4e07\u5343wWkK]?)/); + if (number) { + return parseMetricNumber(number[1], number[2]); + } + } + } + } + return 0; +} + +function parseMetricNumber(raw, unit = '') { + const value = Number(String(raw).replace(/,/g, '')); + if (!Number.isFinite(value)) { + return 0; + } + const normalizedUnit = unit.trim().toLowerCase(); + if (normalizedUnit.includes('\u4e07') || normalizedUnit.includes('w')) { + return Math.round(value * 10000); + } + if (normalizedUnit.includes('\u5343') || normalizedUnit.includes('k')) { + return Math.round(value * 1000); + } + return Math.round(value); +} + +async function pollTasks() { + const response = await fetch(`${DEFAULT_BASE_URL}/v1/media/workers/poll`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-Worker-Token': DEFAULT_WORKER_TOKEN + }, + body: JSON.stringify({ + workerId: DEFAULT_WORKER_ID, + platforms: ['xiaohongshu'], + maxTasks: 5 + }) + }); + if (!response.ok) { + throw new Error(`Poll failed: ${response.status} ${await response.text()}`); + } + return response.json(); +} + +async function submitResult(taskId, body) { + const response = await fetch(`${DEFAULT_BASE_URL}/v1/media/workers/tasks/${taskId}/result`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-Worker-Token': DEFAULT_WORKER_TOKEN + }, + body: JSON.stringify(body) + }); + if (!response.ok) { + throw new Error(`Submit result failed: ${response.status} ${await response.text()}`); + } + return response.json(); +} + +async function handleTask(task) { + const payload = task.payload || {}; + const action = payload.action || (task.taskType === 'SYNC_METRICS' ? 'sync_metrics' : 'check_login'); + if (action === 'check_login') { + const status = await checkLogin(task.accountId || DEFAULT_ACCOUNT_ID); + return submitResult(task.id, { + status: status.loggedIn ? 'SUCCESS' : 'FAILED', + errorMessage: status.loggedIn ? null : status.message, + result: { + action, + platform: 'xiaohongshu', + loggedIn: status.loggedIn, + message: status.message, + url: status.url, + title: status.title, + profileDir: profileDir(task.accountId || DEFAULT_ACCOUNT_ID) + } + }); + } + + if (action === 'sync_metrics') { + const result = await collectMetrics(task); + return submitResult(task.id, result); + } + + return submitResult(task.id, { + status: 'FAILED', + errorMessage: `Unsupported local Xiaohongshu worker action: ${action}`, + result: { action, platform: 'xiaohongshu' } + }); +} + +async function workerOnce() { + const tasks = await pollTasks(); + if (!Array.isArray(tasks) || tasks.length === 0) { + console.log('No pending Xiaohongshu tasks.'); + return; + } + for (const task of tasks) { + const result = await handleTask(task); + console.log(`Completed ${task.id}: ${result.status}`); + } +} + +function sleep(ms) { + return new Promise(resolve => setTimeout(resolve, ms)); +} + +async function workerLoop() { + const intervalMs = Number(process.env.MEDIA_WORKER_INTERVAL_MS || 30000); + console.log(`Xiaohongshu worker loop started. baseUrl=${DEFAULT_BASE_URL}, intervalMs=${intervalMs}`); + while (true) { + try { + await workerOnce(); + } catch (error) { + console.error(error.stack || error.message); + } + await sleep(intervalMs); + } +} + +async function main() { + const mode = process.argv[2]; + if (mode === 'login') { + await login(); + return; + } + if (mode === 'check-login') { + const status = await checkLogin(process.argv[3] || DEFAULT_ACCOUNT_ID); + console.log(JSON.stringify(status, null, 2)); + process.exit(status.loggedIn ? 0 : 2); + } + if (mode === 'metrics') { + const task = { + id: `manual_${Date.now()}`, + taskType: 'SYNC_METRICS', + platform: 'xiaohongshu', + accountId: process.argv[3] || DEFAULT_ACCOUNT_ID, + payload: { action: 'sync_metrics', headless: false } + }; + console.log(JSON.stringify((await collectMetrics(task)).result, null, 2)); + return; + } + if (mode === 'worker') { + await workerOnce(); + return; + } + if (mode === 'worker-loop') { + await workerLoop(); + return; + } + throw new Error('Usage: node scripts/local_xiaohongshu_worker.js [accountId]'); +} + +module.exports = { + checkLogin, + collectMetrics, + login, + profileDir, + workerLoop, + workerOnce +}; + +if (require.main === module) { + main().catch(error => { + console.error(error.stack || error.message); + process.exit(1); + }); +} diff --git a/scripts/start_local_client.ps1 b/scripts/start_local_client.ps1 index d20221f..7c8590f 100644 --- a/scripts/start_local_client.ps1 +++ b/scripts/start_local_client.ps1 @@ -1,8 +1,10 @@ param( [string]$BaseUrl = "http://localhost:8880", [string]$AccountId = "douyin_company", + [string]$XhsAccountId = "xiaohongshu_company", [int]$WorkerIntervalSeconds = 30, - [switch]$RemoteBackend + [switch]$RemoteBackend, + [string]$Platforms = "douyin,xiaohongshu" ) $ErrorActionPreference = "Stop" @@ -115,25 +117,62 @@ if ($RemoteBackend -or -not (Test-LocalBaseUrl $BaseUrl)) { Write-Step "Checking Douyin login" $env:MEDIA_BASE_URL = $BaseUrl $env:DOUYIN_ACCOUNT_ID = $AccountId -$checkOutput = & npm.cmd run douyin:check -- $AccountId 2>&1 -if ($LASTEXITCODE -ne 0) { - Write-Host $checkOutput - Write-Host "" - Write-Host "Douyin login is required. A browser will open. Login, then press Enter in that window." - & npm.cmd run douyin:login -- $AccountId +$platformList = $Platforms.Split(",") | ForEach-Object { $_.Trim().ToLower() } | Where-Object { $_ } + +if ($platformList -contains "douyin") { + $checkOutput = & npm.cmd run douyin:check -- $AccountId 2>&1 if ($LASTEXITCODE -ne 0) { - throw "Douyin login failed." + Write-Host $checkOutput + Write-Host "" + Write-Host "Douyin login is required. A browser will open. Login, then press Enter in that window." + & npm.cmd run douyin:login -- $AccountId + if ($LASTEXITCODE -ne 0) { + throw "Douyin login failed." + } + } else { + Write-Host "Douyin login profile is ready." } -} else { - Write-Host "Douyin login profile is ready." } -Write-Step "Starting Douyin worker loop" -$workerCommand = "Set-Location '$Root'; `$env:MEDIA_BASE_URL='$BaseUrl'; `$env:DOUYIN_ACCOUNT_ID='$AccountId'; `$env:MEDIA_WORKER_INTERVAL_MS='$($WorkerIntervalSeconds * 1000)'; npm.cmd run douyin:worker:loop" -Start-Process powershell.exe -ArgumentList @("-NoExit", "-ExecutionPolicy", "Bypass", "-Command", $workerCommand) -WindowStyle Normal +if ($platformList -contains "xiaohongshu") { + Write-Step "Checking Xiaohongshu login" + $env:XHS_ACCOUNT_ID = $XhsAccountId + $xhsCheckOutput = & npm.cmd run xhs:check -- $XhsAccountId 2>&1 + if ($LASTEXITCODE -ne 0) { + Write-Host $xhsCheckOutput + Write-Host "" + Write-Host "Xiaohongshu login is required. A browser will open. Login, then press Enter in that window." + & npm.cmd run xhs:login -- $XhsAccountId + if ($LASTEXITCODE -ne 0) { + throw "Xiaohongshu login failed." + } + } else { + Write-Host "Xiaohongshu login profile is ready." + } +} + +if ($platformList -contains "douyin") { + Write-Step "Starting Douyin worker loop" + $workerCommand = "Set-Location '$Root'; `$env:MEDIA_BASE_URL='$BaseUrl'; `$env:DOUYIN_ACCOUNT_ID='$AccountId'; `$env:MEDIA_WORKER_INTERVAL_MS='$($WorkerIntervalSeconds * 1000)'; npm.cmd run douyin:worker:loop" + Start-Process powershell.exe -ArgumentList @("-NoExit", "-ExecutionPolicy", "Bypass", "-Command", $workerCommand) -WindowStyle Normal +} + +if ($platformList -contains "xiaohongshu") { + Write-Step "Starting Xiaohongshu worker loop" + $xhsWorkerCommand = "Set-Location '$Root'; `$env:MEDIA_BASE_URL='$BaseUrl'; `$env:XHS_ACCOUNT_ID='$XhsAccountId'; `$env:MEDIA_WORKER_INTERVAL_MS='$($WorkerIntervalSeconds * 1000)'; npm.cmd run xhs:worker:loop" + Start-Process powershell.exe -ArgumentList @("-NoExit", "-ExecutionPolicy", "Bypass", "-Command", $xhsWorkerCommand) -WindowStyle Normal +} + +if (($platformList -contains "douyin") -and ($platformList -contains "xiaohongshu")) { + Write-Step "Starting media matrix worker loop" + $matrixWorkerCommand = "Set-Location '$Root'; `$env:MEDIA_BASE_URL='$BaseUrl'; `$env:DOUYIN_ACCOUNT_ID='$AccountId'; `$env:XHS_ACCOUNT_ID='$XhsAccountId'; `$env:MEDIA_WORKER_INTERVAL_MS='$($WorkerIntervalSeconds * 1000)'; npm.cmd run media:worker:loop" + Start-Process powershell.exe -ArgumentList @("-NoExit", "-ExecutionPolicy", "Bypass", "-Command", $matrixWorkerCommand) -WindowStyle Normal +} Write-Host "" Write-Host "Local client is ready." Write-Host "Backend: $BaseUrl" -Write-Host "Account: $AccountId" +Write-Host "Douyin account: $AccountId" +Write-Host "Xiaohongshu account: $XhsAccountId" +Write-Host "Platforms: $($platformList -join ', ')" Write-Host "Worker polling interval: $WorkerIntervalSeconds seconds" diff --git a/src/main/java/com/company/agentfoundation/media/model/MediaTask.java b/src/main/java/com/company/agentfoundation/media/model/MediaTask.java index 5c7c25b..f945dad 100644 --- a/src/main/java/com/company/agentfoundation/media/model/MediaTask.java +++ b/src/main/java/com/company/agentfoundation/media/model/MediaTask.java @@ -67,48 +67,47 @@ public class MediaTask { long shares = 0; List> rows = new ArrayList<>(); List> videoRows = new ArrayList<>(); + Map byPlatform = new LinkedHashMap<>(); for (MediaTask task : tasks) { if (task.status != MediaTaskStatus.SUCCESS || task.result == null) { continue; } + Map platformResults = mapValue(task.result.get("platformResults")); + if (!platformResults.isEmpty()) { + for (Map.Entry entry : platformResults.entrySet()) { + Map platformResult = mapValue(entry.getValue()); + Map metrics = mapValue(platformResult.get("metrics")); + if (metrics.isEmpty()) { + continue; + } + MetricValues values = metricValues(metrics); + views += values.views; + likes += values.likes; + comments += values.comments; + collects += values.collects; + shares += values.shares; + String platform = entry.getKey(); + String accountId = stringValue(platformResult.get("accountId")); + byPlatform.computeIfAbsent(platform, ignored -> new MetricBucket()).add(values); + rows.add(metricRow(task.id, platform, accountId, values)); + addVideoRows(videoRows, task, platform, accountId, listOfMaps(platformResult.get("videos"))); + } + continue; + } Map metrics = mapValue(task.result.get("metrics")); if (metrics.isEmpty()) { continue; } - long taskViews = longValue(metrics.get("views")); - long taskLikes = longValue(metrics.get("likes")); - long taskComments = longValue(metrics.get("comments")); - long taskCollects = longValue(metrics.get("collects")); - long taskShares = longValue(metrics.get("shares")); - views += taskViews; - likes += taskLikes; - comments += taskComments; - collects += taskCollects; - shares += taskShares; - rows.add(Map.of( - "taskId", task.id, - "platform", task.platform, - "accountId", task.accountId, - "views", taskViews, - "likes", taskLikes, - "comments", taskComments, - "collects", taskCollects, - "shares", taskShares - )); - for (Map video : listOfMaps(task.result.get("videos"))) { - videoRows.add(Map.of( - "taskId", task.id, - "platform", task.platform, - "accountId", task.accountId, - "title", stringValue(video.get("title")), - "href", stringValue(video.get("href")), - "views", longValue(video.get("views")), - "likes", longValue(video.get("likes")), - "comments", longValue(video.get("comments")), - "collects", longValue(video.get("collects")), - "shares", longValue(video.get("shares")) - )); - } + MetricValues values = metricValues(metrics); + views += values.views; + likes += values.likes; + comments += values.comments; + collects += values.collects; + shares += values.shares; + byPlatform.computeIfAbsent(task.platform, ignored -> new MetricBucket()) + .add(values); + rows.add(metricRow(task.id, task.platform, task.accountId, values)); + addVideoRows(videoRows, task, task.platform, task.accountId, listOfMaps(task.result.get("videos"))); } Map summary = new LinkedHashMap<>(); summary.put("views", views); @@ -121,6 +120,7 @@ public class MediaTask { summary.put("rows", rows); summary.put("videoCount", videoRows.size()); summary.put("videoRows", videoRows); + summary.put("byPlatform", platformSummary(byPlatform)); return summary; } @@ -159,4 +159,96 @@ public class MediaTask { private static String stringValue(Object value) { return value == null ? "" : String.valueOf(value); } + + private static MetricValues metricValues(Map metrics) { + return new MetricValues( + longValue(metrics.get("views")), + longValue(metrics.get("likes")), + longValue(metrics.get("comments")), + longValue(metrics.get("collects")), + longValue(metrics.get("shares")) + ); + } + + private static Map metricRow(String taskId, String platform, String accountId, MetricValues values) { + return Map.of( + "taskId", taskId, + "platform", platform, + "accountId", accountId, + "views", values.views, + "likes", values.likes, + "comments", values.comments, + "collects", values.collects, + "shares", values.shares + ); + } + + private static void addVideoRows( + List> videoRows, + MediaTask task, + String platform, + String accountId, + List> videos + ) { + for (Map video : videos) { + videoRows.add(Map.of( + "taskId", task.id, + "platform", platform, + "accountId", accountId, + "title", stringValue(video.get("title")), + "href", stringValue(video.get("href")), + "views", longValue(video.get("views")), + "likes", longValue(video.get("likes")), + "comments", longValue(video.get("comments")), + "collects", longValue(video.get("collects")), + "shares", longValue(video.get("shares")) + )); + } + } + + private static Map platformSummary(Map buckets) { + Map result = new LinkedHashMap<>(); + for (Map.Entry entry : buckets.entrySet()) { + result.put(entry.getKey(), entry.getValue().toMap()); + } + return result; + } + + private static class MetricBucket { + private long views; + private long likes; + private long comments; + private long collects; + private long shares; + + private void add(MetricValues values) { + this.views += values.views; + this.likes += values.likes; + this.comments += values.comments; + this.collects += values.collects; + this.shares += values.shares; + } + + private Map toMap() { + long interactionCount = likes + comments + collects + shares; + Map map = new LinkedHashMap<>(); + map.put("views", views); + map.put("likes", likes); + map.put("comments", comments); + map.put("collects", collects); + map.put("shares", shares); + map.put("interactionCount", interactionCount); + map.put("interactionRate", views <= 0 ? 0.0 : interactionCount * 1.0 / views); + return map; + } + } + + private record MetricValues( + long views, + long likes, + long comments, + long collects, + long shares + ) { + } } diff --git a/start_yuedong_client.bat b/start_yuedong_client.bat index df335ef..863c5a0 100644 --- a/start_yuedong_client.bat +++ b/start_yuedong_client.bat @@ -1,5 +1,5 @@ @echo off setlocal cd /d "%~dp0" -powershell.exe -NoProfile -ExecutionPolicy Bypass -File "%~dp0scripts\start_local_client.ps1" -BaseUrl "http://agent.yuedongsports.com" -RemoteBackend +powershell.exe -NoProfile -ExecutionPolicy Bypass -File "%~dp0scripts\start_local_client.ps1" -BaseUrl "http://agent.yuedongsports.com" -RemoteBackend -Platforms "douyin,xiaohongshu" pause