抖音数据获取
This commit is contained in:
parent
836d6b7e34
commit
b587576e85
2
.gitignore
vendored
2
.gitignore
vendored
@ -1,3 +1,5 @@
|
||||
/target/
|
||||
/.idea/
|
||||
*.iml
|
||||
/node_modules/
|
||||
/data/platform-profiles/
|
||||
|
||||
19
CLIENT_README.txt
Normal file
19
CLIENT_README.txt
Normal file
@ -0,0 +1,19 @@
|
||||
Yuedong short-video local worker
|
||||
|
||||
Before first use, install:
|
||||
1. Node.js LTS
|
||||
2. Microsoft Edge or Google Chrome
|
||||
|
||||
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.
|
||||
4. Keep the worker terminal window open while using the system.
|
||||
|
||||
Server API:
|
||||
http://agent.yuedongsports.com
|
||||
|
||||
Local login data is stored only on this computer under:
|
||||
data\platform-profiles\
|
||||
|
||||
Do not copy data\platform-profiles\ to another person.
|
||||
BIN
dist/agent-foundation-client-yuedong.zip
vendored
Normal file
BIN
dist/agent-foundation-client-yuedong.zip
vendored
Normal file
Binary file not shown.
19
dist/agent-foundation-client-yuedong/CLIENT_README.txt
vendored
Normal file
19
dist/agent-foundation-client-yuedong/CLIENT_README.txt
vendored
Normal file
@ -0,0 +1,19 @@
|
||||
Yuedong short-video local worker
|
||||
|
||||
Before first use, install:
|
||||
1. Node.js LTS
|
||||
2. Microsoft Edge or Google Chrome
|
||||
|
||||
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.
|
||||
4. Keep the worker terminal window open while using the system.
|
||||
|
||||
Server API:
|
||||
http://agent.yuedongsports.com
|
||||
|
||||
Local login data is stored only on this computer under:
|
||||
data\platform-profiles\
|
||||
|
||||
Do not copy data\platform-profiles\ to another person.
|
||||
57
dist/agent-foundation-client-yuedong/package-lock.json
generated
vendored
Normal file
57
dist/agent-foundation-client-yuedong/package-lock.json
generated
vendored
Normal file
@ -0,0 +1,57 @@
|
||||
{
|
||||
"name": "agent-foundation-local-worker",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "agent-foundation-local-worker",
|
||||
"dependencies": {
|
||||
"playwright": "^1.61.0"
|
||||
}
|
||||
},
|
||||
"node_modules/fsevents": {
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.2.tgz",
|
||||
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/playwright": {
|
||||
"version": "1.61.0",
|
||||
"resolved": "https://registry.npmmirror.com/playwright/-/playwright-1.61.0.tgz",
|
||||
"integrity": "sha512-Z+7BeeqQPRRzklHsVFP4KTGIyMxKUmfeRA4WisM6G3/XW6nwGeX6fX9qYaDa+CiUqpOkb2f6X3nar05R3kSuJQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"playwright-core": "1.61.0"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "2.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/playwright-core": {
|
||||
"version": "1.61.0",
|
||||
"resolved": "https://registry.npmmirror.com/playwright-core/-/playwright-core-1.61.0.tgz",
|
||||
"integrity": "sha512-caX7TrY3Ml6egyDX0WUcTHDxodl/b51y5wJOdCEA36QviK/s2g081hvmGs8eaE3DWb6NYZQ6BjO/QkNRPenoPA==",
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"playwright-core": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
15
dist/agent-foundation-client-yuedong/package.json
vendored
Normal file
15
dist/agent-foundation-client-yuedong/package.json
vendored
Normal file
@ -0,0 +1,15 @@
|
||||
{
|
||||
"name": "agent-foundation-local-worker",
|
||||
"private": true,
|
||||
"type": "commonjs",
|
||||
"scripts": {
|
||||
"douyin:login": "node scripts/local_douyin_worker.js login",
|
||||
"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"
|
||||
},
|
||||
"dependencies": {
|
||||
"playwright": "^1.61.0"
|
||||
}
|
||||
}
|
||||
507
dist/agent-foundation-client-yuedong/scripts/local_douyin_worker.js
vendored
Normal file
507
dist/agent-foundation-client-yuedong/scripts/local_douyin_worker.js
vendored
Normal file
@ -0,0 +1,507 @@
|
||||
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-douyin-worker';
|
||||
const DEFAULT_WORKER_TOKEN = process.env.MEDIA_WORKER_TOKEN || 'local-worker-dev-token';
|
||||
const DEFAULT_ACCOUNT_ID = process.env.DOUYIN_ACCOUNT_ID || 'douyin_company';
|
||||
const DEFAULT_CREATOR_URL = process.env.DOUYIN_CREATOR_URL || 'https://creator.douyin.com';
|
||||
const DEFAULT_CONTENT_URL = process.env.DOUYIN_CONTENT_URL || 'https://creator.douyin.com/creator-micro/content/manage';
|
||||
const DEFAULT_PROFILE_ROOT = process.env.DOUYIN_PROFILE_ROOT || path.resolve('data/platform-profiles');
|
||||
|
||||
const LOGIN_TEXT = new RegExp([
|
||||
'\\u626b\\u7801\\u767b\\u5f55',
|
||||
'\\u9a8c\\u8bc1\\u7801',
|
||||
'\\u624b\\u673a\\u53f7',
|
||||
'\\u767b\\u5f55'
|
||||
].join('|'));
|
||||
|
||||
const CREATOR_TEXT = new RegExp([
|
||||
'\\u521b\\u4f5c\\u670d\\u52a1',
|
||||
'\\u53d1\\u5e03\\u4f5c\\u54c1',
|
||||
'\\u5185\\u5bb9\\u7ba1\\u7406',
|
||||
'\\u6570\\u636e\\u4e2d\\u5fc3',
|
||||
'\\u4f5c\\u54c1\\u7ba1\\u7406',
|
||||
'\\u53d1\\u5e03\\u89c6\\u9891'
|
||||
].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.DOUYIN_BROWSER_CHANNEL) {
|
||||
return process.env.DOUYIN_BROWSER_CHANNEL;
|
||||
}
|
||||
return process.platform === 'win32' ? 'msedge' : undefined;
|
||||
}
|
||||
|
||||
async function launchDouyinContext({ 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 launchDouyinContext({ accountId, headless: false });
|
||||
const page = context.pages()[0] || await context.newPage();
|
||||
await page.goto(DEFAULT_CREATOR_URL, { waitUntil: 'domcontentloaded', timeout: 60000 });
|
||||
console.log(`Douyin login profile: ${profileDir(accountId)}`);
|
||||
console.log('Please scan/login in the opened browser. Keep this window open until login succeeds.');
|
||||
await waitForEnter('Press Enter here after Douyin 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 launchDouyinContext({ 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 && /creator\.douyin\.com/.test(url));
|
||||
return {
|
||||
loggedIn,
|
||||
status: loggedIn ? 'SUCCESS' : 'FAILED',
|
||||
message: loggedIn ? 'Douyin creator profile is logged in.' : 'Douyin 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 launchDouyinContext({ 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 || 5000));
|
||||
const status = await detectLogin(page, accountId);
|
||||
if (!status.loggedIn) {
|
||||
return {
|
||||
status: 'FAILED',
|
||||
errorMessage: status.message,
|
||||
result: {
|
||||
action: 'sync_metrics',
|
||||
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',
|
||||
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/douyin');
|
||||
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, ['\\u64ad\\u653e', '\\u6d4f\\u89c8', '\\u89c2\\u770b', '\\u5c55\\u73b0']),
|
||||
likes: findMetric(compact, ['\\u70b9\\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*="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 < 160 || rect.height < 40) {
|
||||
continue;
|
||||
}
|
||||
const text = (element.innerText || '').replace(/\s+/g, ' ').trim();
|
||||
if (text.length < 12 || 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 : '',
|
||||
rect: {
|
||||
x: Math.round(rect.x),
|
||||
y: Math.round(rect.y),
|
||||
width: Math.round(rect.width),
|
||||
height: Math.round(rect.height)
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
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 = extractVideoTitle(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 extractVideoTitle(text) {
|
||||
const cleaned = text
|
||||
.replace(/\d{4}[-/.]\d{1,2}[-/.]\d{1,2}.*/g, '')
|
||||
.replace(/(\u64ad\u653e|\u6d4f\u89c8|\u89c2\u770b|\u5c55\u73b0|\u70b9\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: ['douyin'],
|
||||
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,
|
||||
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 Douyin worker action: ${action}`,
|
||||
result: { action }
|
||||
});
|
||||
}
|
||||
|
||||
async function workerOnce() {
|
||||
const tasks = await pollTasks();
|
||||
if (!Array.isArray(tasks) || tasks.length === 0) {
|
||||
console.log('No pending Douyin 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(`Douyin 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: 'douyin',
|
||||
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_douyin_worker.js <login|check-login|metrics|worker|worker-loop> [accountId]');
|
||||
}
|
||||
|
||||
main().catch(error => {
|
||||
console.error(error.stack || error.message);
|
||||
process.exit(1);
|
||||
});
|
||||
139
dist/agent-foundation-client-yuedong/scripts/start_local_client.ps1
vendored
Normal file
139
dist/agent-foundation-client-yuedong/scripts/start_local_client.ps1
vendored
Normal file
@ -0,0 +1,139 @@
|
||||
param(
|
||||
[string]$BaseUrl = "http://localhost:8880",
|
||||
[string]$AccountId = "douyin_company",
|
||||
[int]$WorkerIntervalSeconds = 30,
|
||||
[switch]$RemoteBackend
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
$Root = Resolve-Path (Join-Path $PSScriptRoot "..")
|
||||
Set-Location $Root
|
||||
|
||||
function Write-Step([string]$Message) {
|
||||
Write-Host ""
|
||||
Write-Host "==> $Message" -ForegroundColor Cyan
|
||||
}
|
||||
|
||||
function Require-Command([string]$Name, [string]$InstallHint) {
|
||||
if (-not (Get-Command $Name -ErrorAction SilentlyContinue)) {
|
||||
throw "$Name is not installed or not in PATH. $InstallHint"
|
||||
}
|
||||
}
|
||||
|
||||
function Test-BackendHealth([string]$Url) {
|
||||
try {
|
||||
$response = Invoke-WebRequest -UseBasicParsing -Uri "$Url/actuator/health" -TimeoutSec 3
|
||||
return $response.StatusCode -ge 200 -and $response.StatusCode -lt 300
|
||||
} catch {
|
||||
return $false
|
||||
}
|
||||
}
|
||||
|
||||
function Find-BackendJar {
|
||||
$jars = Get-ChildItem -Path (Join-Path $Root "target") -Filter "agent-foundation-*.jar" -ErrorAction SilentlyContinue |
|
||||
Where-Object { $_.Name -notlike "*.original" } |
|
||||
Sort-Object LastWriteTime -Descending
|
||||
if ($jars.Count -eq 0) {
|
||||
return $null
|
||||
}
|
||||
return $jars[0].FullName
|
||||
}
|
||||
|
||||
function Test-BrowserInstalled {
|
||||
$paths = @(
|
||||
"$env:ProgramFiles\Microsoft\Edge\Application\msedge.exe",
|
||||
"${env:ProgramFiles(x86)}\Microsoft\Edge\Application\msedge.exe",
|
||||
"$env:ProgramFiles\Google\Chrome\Application\chrome.exe",
|
||||
"${env:ProgramFiles(x86)}\Google\Chrome\Application\chrome.exe"
|
||||
)
|
||||
foreach ($path in $paths) {
|
||||
if ($path -and (Test-Path $path)) {
|
||||
return $true
|
||||
}
|
||||
}
|
||||
return $false
|
||||
}
|
||||
|
||||
function Test-LocalBaseUrl([string]$Url) {
|
||||
return $Url -match "^https?://(localhost|127\.0\.0\.1)(:\d+)?/?$"
|
||||
}
|
||||
|
||||
Write-Step "Checking local runtime"
|
||||
Require-Command "node" "Install Node.js LTS from https://nodejs.org/"
|
||||
Require-Command "npm.cmd" "Install Node.js LTS from https://nodejs.org/"
|
||||
if (-not $RemoteBackend) {
|
||||
Require-Command "java" "Install JDK 17+ or JRE 17+."
|
||||
}
|
||||
if (-not (Test-BrowserInstalled)) {
|
||||
throw "Microsoft Edge or Google Chrome is required."
|
||||
}
|
||||
|
||||
Write-Step "Installing Node dependencies if needed"
|
||||
if (-not (Test-Path (Join-Path $Root "node_modules\playwright"))) {
|
||||
& npm.cmd install
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "npm install failed."
|
||||
}
|
||||
} else {
|
||||
Write-Host "Node dependencies already installed."
|
||||
}
|
||||
|
||||
if ($RemoteBackend -or -not (Test-LocalBaseUrl $BaseUrl)) {
|
||||
Write-Step "Checking remote backend"
|
||||
if (Test-BackendHealth $BaseUrl) {
|
||||
Write-Host "Remote backend is reachable: $BaseUrl"
|
||||
} else {
|
||||
Write-Host "Remote backend health check did not return OK. Continuing because /actuator/health may be disabled."
|
||||
Write-Host "Worker will connect to: $BaseUrl"
|
||||
}
|
||||
} else {
|
||||
Write-Step "Starting backend service if needed"
|
||||
if (Test-BackendHealth $BaseUrl) {
|
||||
Write-Host "Backend is already running: $BaseUrl"
|
||||
} else {
|
||||
$jar = Find-BackendJar
|
||||
if (-not $jar) {
|
||||
throw "Backend jar not found under target\. Build the Spring Boot jar first."
|
||||
}
|
||||
$backendCommand = "Set-Location '$Root'; java -jar '$jar'"
|
||||
Start-Process powershell.exe -ArgumentList @("-NoExit", "-ExecutionPolicy", "Bypass", "-Command", $backendCommand) -WindowStyle Normal
|
||||
Write-Host "Waiting for backend health..."
|
||||
$ready = $false
|
||||
for ($i = 0; $i -lt 40; $i++) {
|
||||
Start-Sleep -Seconds 2
|
||||
if (Test-BackendHealth $BaseUrl) {
|
||||
$ready = $true
|
||||
break
|
||||
}
|
||||
}
|
||||
if (-not $ready) {
|
||||
throw "Backend did not become healthy at $BaseUrl. Check the backend window."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "Douyin login failed."
|
||||
}
|
||||
} 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
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "Local client is ready."
|
||||
Write-Host "Backend: $BaseUrl"
|
||||
Write-Host "Account: $AccountId"
|
||||
Write-Host "Worker polling interval: $WorkerIntervalSeconds seconds"
|
||||
5
dist/agent-foundation-client-yuedong/start_yuedong_client.bat
vendored
Normal file
5
dist/agent-foundation-client-yuedong/start_yuedong_client.bat
vendored
Normal file
@ -0,0 +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
|
||||
pause
|
||||
57
package-lock.json
generated
Normal file
57
package-lock.json
generated
Normal file
@ -0,0 +1,57 @@
|
||||
{
|
||||
"name": "agent-foundation-local-worker",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "agent-foundation-local-worker",
|
||||
"dependencies": {
|
||||
"playwright": "^1.61.0"
|
||||
}
|
||||
},
|
||||
"node_modules/fsevents": {
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.2.tgz",
|
||||
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/playwright": {
|
||||
"version": "1.61.0",
|
||||
"resolved": "https://registry.npmmirror.com/playwright/-/playwright-1.61.0.tgz",
|
||||
"integrity": "sha512-Z+7BeeqQPRRzklHsVFP4KTGIyMxKUmfeRA4WisM6G3/XW6nwGeX6fX9qYaDa+CiUqpOkb2f6X3nar05R3kSuJQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"playwright-core": "1.61.0"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "2.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/playwright-core": {
|
||||
"version": "1.61.0",
|
||||
"resolved": "https://registry.npmmirror.com/playwright-core/-/playwright-core-1.61.0.tgz",
|
||||
"integrity": "sha512-caX7TrY3Ml6egyDX0WUcTHDxodl/b51y5wJOdCEA36QviK/s2g081hvmGs8eaE3DWb6NYZQ6BjO/QkNRPenoPA==",
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"playwright-core": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
15
package.json
Normal file
15
package.json
Normal file
@ -0,0 +1,15 @@
|
||||
{
|
||||
"name": "agent-foundation-local-worker",
|
||||
"private": true,
|
||||
"type": "commonjs",
|
||||
"scripts": {
|
||||
"douyin:login": "node scripts/local_douyin_worker.js login",
|
||||
"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"
|
||||
},
|
||||
"dependencies": {
|
||||
"playwright": "^1.61.0"
|
||||
}
|
||||
}
|
||||
507
scripts/local_douyin_worker.js
Normal file
507
scripts/local_douyin_worker.js
Normal file
@ -0,0 +1,507 @@
|
||||
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-douyin-worker';
|
||||
const DEFAULT_WORKER_TOKEN = process.env.MEDIA_WORKER_TOKEN || 'local-worker-dev-token';
|
||||
const DEFAULT_ACCOUNT_ID = process.env.DOUYIN_ACCOUNT_ID || 'douyin_company';
|
||||
const DEFAULT_CREATOR_URL = process.env.DOUYIN_CREATOR_URL || 'https://creator.douyin.com';
|
||||
const DEFAULT_CONTENT_URL = process.env.DOUYIN_CONTENT_URL || 'https://creator.douyin.com/creator-micro/content/manage';
|
||||
const DEFAULT_PROFILE_ROOT = process.env.DOUYIN_PROFILE_ROOT || path.resolve('data/platform-profiles');
|
||||
|
||||
const LOGIN_TEXT = new RegExp([
|
||||
'\\u626b\\u7801\\u767b\\u5f55',
|
||||
'\\u9a8c\\u8bc1\\u7801',
|
||||
'\\u624b\\u673a\\u53f7',
|
||||
'\\u767b\\u5f55'
|
||||
].join('|'));
|
||||
|
||||
const CREATOR_TEXT = new RegExp([
|
||||
'\\u521b\\u4f5c\\u670d\\u52a1',
|
||||
'\\u53d1\\u5e03\\u4f5c\\u54c1',
|
||||
'\\u5185\\u5bb9\\u7ba1\\u7406',
|
||||
'\\u6570\\u636e\\u4e2d\\u5fc3',
|
||||
'\\u4f5c\\u54c1\\u7ba1\\u7406',
|
||||
'\\u53d1\\u5e03\\u89c6\\u9891'
|
||||
].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.DOUYIN_BROWSER_CHANNEL) {
|
||||
return process.env.DOUYIN_BROWSER_CHANNEL;
|
||||
}
|
||||
return process.platform === 'win32' ? 'msedge' : undefined;
|
||||
}
|
||||
|
||||
async function launchDouyinContext({ 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 launchDouyinContext({ accountId, headless: false });
|
||||
const page = context.pages()[0] || await context.newPage();
|
||||
await page.goto(DEFAULT_CREATOR_URL, { waitUntil: 'domcontentloaded', timeout: 60000 });
|
||||
console.log(`Douyin login profile: ${profileDir(accountId)}`);
|
||||
console.log('Please scan/login in the opened browser. Keep this window open until login succeeds.');
|
||||
await waitForEnter('Press Enter here after Douyin 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 launchDouyinContext({ 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 && /creator\.douyin\.com/.test(url));
|
||||
return {
|
||||
loggedIn,
|
||||
status: loggedIn ? 'SUCCESS' : 'FAILED',
|
||||
message: loggedIn ? 'Douyin creator profile is logged in.' : 'Douyin 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 launchDouyinContext({ 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 || 5000));
|
||||
const status = await detectLogin(page, accountId);
|
||||
if (!status.loggedIn) {
|
||||
return {
|
||||
status: 'FAILED',
|
||||
errorMessage: status.message,
|
||||
result: {
|
||||
action: 'sync_metrics',
|
||||
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',
|
||||
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/douyin');
|
||||
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, ['\\u64ad\\u653e', '\\u6d4f\\u89c8', '\\u89c2\\u770b', '\\u5c55\\u73b0']),
|
||||
likes: findMetric(compact, ['\\u70b9\\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*="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 < 160 || rect.height < 40) {
|
||||
continue;
|
||||
}
|
||||
const text = (element.innerText || '').replace(/\s+/g, ' ').trim();
|
||||
if (text.length < 12 || 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 : '',
|
||||
rect: {
|
||||
x: Math.round(rect.x),
|
||||
y: Math.round(rect.y),
|
||||
width: Math.round(rect.width),
|
||||
height: Math.round(rect.height)
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
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 = extractVideoTitle(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 extractVideoTitle(text) {
|
||||
const cleaned = text
|
||||
.replace(/\d{4}[-/.]\d{1,2}[-/.]\d{1,2}.*/g, '')
|
||||
.replace(/(\u64ad\u653e|\u6d4f\u89c8|\u89c2\u770b|\u5c55\u73b0|\u70b9\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: ['douyin'],
|
||||
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,
|
||||
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 Douyin worker action: ${action}`,
|
||||
result: { action }
|
||||
});
|
||||
}
|
||||
|
||||
async function workerOnce() {
|
||||
const tasks = await pollTasks();
|
||||
if (!Array.isArray(tasks) || tasks.length === 0) {
|
||||
console.log('No pending Douyin 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(`Douyin 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: 'douyin',
|
||||
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_douyin_worker.js <login|check-login|metrics|worker|worker-loop> [accountId]');
|
||||
}
|
||||
|
||||
main().catch(error => {
|
||||
console.error(error.stack || error.message);
|
||||
process.exit(1);
|
||||
});
|
||||
71
scripts/local_media_worker.ps1
Normal file
71
scripts/local_media_worker.ps1
Normal file
@ -0,0 +1,71 @@
|
||||
param(
|
||||
[string]$BaseUrl = "http://localhost:8880",
|
||||
[string]$WorkerId = "local-worker-01",
|
||||
[string]$WorkerToken = "local-worker-dev-token"
|
||||
)
|
||||
|
||||
$headers = @{
|
||||
"X-Worker-Token" = $WorkerToken
|
||||
}
|
||||
|
||||
$pollBody = @{
|
||||
workerId = $WorkerId
|
||||
platforms = @("douyin", "xhs", "wxvideo", "kuaishou", "bilibili", "weibo")
|
||||
maxTasks = 5
|
||||
} | ConvertTo-Json
|
||||
|
||||
$tasks = Invoke-RestMethod `
|
||||
-Uri "$BaseUrl/v1/media/workers/poll" `
|
||||
-Method Post `
|
||||
-Headers $headers `
|
||||
-ContentType "application/json" `
|
||||
-Body $pollBody
|
||||
|
||||
foreach ($task in $tasks) {
|
||||
if ($task.taskType -eq "SYNC_COMMENTS") {
|
||||
$result = @{
|
||||
status = "SUCCESS"
|
||||
result = @{
|
||||
comments = @(
|
||||
@{
|
||||
platform = $task.platform
|
||||
accountId = $task.accountId
|
||||
userName = "测试用户"
|
||||
content = "这个设备多少钱?"
|
||||
likedCount = 12
|
||||
intent = "sales_lead"
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
} elseif ($task.taskType -eq "SYNC_METRICS") {
|
||||
$result = @{
|
||||
status = "SUCCESS"
|
||||
result = @{
|
||||
metrics = @{
|
||||
views = 12000
|
||||
likes = 860
|
||||
comments = 56
|
||||
collects = 128
|
||||
shares = 43
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$result = @{
|
||||
status = "SUCCESS"
|
||||
result = @{
|
||||
message = "local worker mock completed"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Invoke-RestMethod `
|
||||
-Uri "$BaseUrl/v1/media/workers/tasks/$($task.id)/result" `
|
||||
-Method Post `
|
||||
-Headers $headers `
|
||||
-ContentType "application/json" `
|
||||
-Body ($result | ConvertTo-Json -Depth 10) | Out-Null
|
||||
|
||||
Write-Host "completed $($task.id) $($task.taskType) $($task.platform)"
|
||||
}
|
||||
139
scripts/start_local_client.ps1
Normal file
139
scripts/start_local_client.ps1
Normal file
@ -0,0 +1,139 @@
|
||||
param(
|
||||
[string]$BaseUrl = "http://localhost:8880",
|
||||
[string]$AccountId = "douyin_company",
|
||||
[int]$WorkerIntervalSeconds = 30,
|
||||
[switch]$RemoteBackend
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
$Root = Resolve-Path (Join-Path $PSScriptRoot "..")
|
||||
Set-Location $Root
|
||||
|
||||
function Write-Step([string]$Message) {
|
||||
Write-Host ""
|
||||
Write-Host "==> $Message" -ForegroundColor Cyan
|
||||
}
|
||||
|
||||
function Require-Command([string]$Name, [string]$InstallHint) {
|
||||
if (-not (Get-Command $Name -ErrorAction SilentlyContinue)) {
|
||||
throw "$Name is not installed or not in PATH. $InstallHint"
|
||||
}
|
||||
}
|
||||
|
||||
function Test-BackendHealth([string]$Url) {
|
||||
try {
|
||||
$response = Invoke-WebRequest -UseBasicParsing -Uri "$Url/actuator/health" -TimeoutSec 3
|
||||
return $response.StatusCode -ge 200 -and $response.StatusCode -lt 300
|
||||
} catch {
|
||||
return $false
|
||||
}
|
||||
}
|
||||
|
||||
function Find-BackendJar {
|
||||
$jars = Get-ChildItem -Path (Join-Path $Root "target") -Filter "agent-foundation-*.jar" -ErrorAction SilentlyContinue |
|
||||
Where-Object { $_.Name -notlike "*.original" } |
|
||||
Sort-Object LastWriteTime -Descending
|
||||
if ($jars.Count -eq 0) {
|
||||
return $null
|
||||
}
|
||||
return $jars[0].FullName
|
||||
}
|
||||
|
||||
function Test-BrowserInstalled {
|
||||
$paths = @(
|
||||
"$env:ProgramFiles\Microsoft\Edge\Application\msedge.exe",
|
||||
"${env:ProgramFiles(x86)}\Microsoft\Edge\Application\msedge.exe",
|
||||
"$env:ProgramFiles\Google\Chrome\Application\chrome.exe",
|
||||
"${env:ProgramFiles(x86)}\Google\Chrome\Application\chrome.exe"
|
||||
)
|
||||
foreach ($path in $paths) {
|
||||
if ($path -and (Test-Path $path)) {
|
||||
return $true
|
||||
}
|
||||
}
|
||||
return $false
|
||||
}
|
||||
|
||||
function Test-LocalBaseUrl([string]$Url) {
|
||||
return $Url -match "^https?://(localhost|127\.0\.0\.1)(:\d+)?/?$"
|
||||
}
|
||||
|
||||
Write-Step "Checking local runtime"
|
||||
Require-Command "node" "Install Node.js LTS from https://nodejs.org/"
|
||||
Require-Command "npm.cmd" "Install Node.js LTS from https://nodejs.org/"
|
||||
if (-not $RemoteBackend) {
|
||||
Require-Command "java" "Install JDK 17+ or JRE 17+."
|
||||
}
|
||||
if (-not (Test-BrowserInstalled)) {
|
||||
throw "Microsoft Edge or Google Chrome is required."
|
||||
}
|
||||
|
||||
Write-Step "Installing Node dependencies if needed"
|
||||
if (-not (Test-Path (Join-Path $Root "node_modules\playwright"))) {
|
||||
& npm.cmd install
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "npm install failed."
|
||||
}
|
||||
} else {
|
||||
Write-Host "Node dependencies already installed."
|
||||
}
|
||||
|
||||
if ($RemoteBackend -or -not (Test-LocalBaseUrl $BaseUrl)) {
|
||||
Write-Step "Checking remote backend"
|
||||
if (Test-BackendHealth $BaseUrl) {
|
||||
Write-Host "Remote backend is reachable: $BaseUrl"
|
||||
} else {
|
||||
Write-Host "Remote backend health check did not return OK. Continuing because /actuator/health may be disabled."
|
||||
Write-Host "Worker will connect to: $BaseUrl"
|
||||
}
|
||||
} else {
|
||||
Write-Step "Starting backend service if needed"
|
||||
if (Test-BackendHealth $BaseUrl) {
|
||||
Write-Host "Backend is already running: $BaseUrl"
|
||||
} else {
|
||||
$jar = Find-BackendJar
|
||||
if (-not $jar) {
|
||||
throw "Backend jar not found under target\. Build the Spring Boot jar first."
|
||||
}
|
||||
$backendCommand = "Set-Location '$Root'; java -jar '$jar'"
|
||||
Start-Process powershell.exe -ArgumentList @("-NoExit", "-ExecutionPolicy", "Bypass", "-Command", $backendCommand) -WindowStyle Normal
|
||||
Write-Host "Waiting for backend health..."
|
||||
$ready = $false
|
||||
for ($i = 0; $i -lt 40; $i++) {
|
||||
Start-Sleep -Seconds 2
|
||||
if (Test-BackendHealth $BaseUrl) {
|
||||
$ready = $true
|
||||
break
|
||||
}
|
||||
}
|
||||
if (-not $ready) {
|
||||
throw "Backend did not become healthy at $BaseUrl. Check the backend window."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "Douyin login failed."
|
||||
}
|
||||
} 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
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "Local client is ready."
|
||||
Write-Host "Backend: $BaseUrl"
|
||||
Write-Host "Account: $AccountId"
|
||||
Write-Host "Worker polling interval: $WorkerIntervalSeconds seconds"
|
||||
@ -1,12 +1,12 @@
|
||||
package com.company.agentfoundation.agent.capability;
|
||||
|
||||
import com.company.agentfoundation.agent.AgentCapabilityResponse;
|
||||
import com.company.agentfoundation.agent.AgentIntent;
|
||||
import com.company.agentfoundation.agent.AgentRequestContext;
|
||||
import com.company.agentfoundation.agent.report.dto.AgentCapabilityResponse;
|
||||
import com.company.agentfoundation.agent.report.model.AgentIntent;
|
||||
import com.company.agentfoundation.agent.report.model.AgentRequestContext;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
abstract class AbstractPlaceholderCapabilityHandler implements AgentCapabilityHandler {
|
||||
public abstract class AbstractPlaceholderCapabilityHandler implements AgentCapabilityHandler {
|
||||
private final AgentIntent intent;
|
||||
private final String message;
|
||||
private final List<String> dataSources;
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
package com.company.agentfoundation.agent.capability;
|
||||
|
||||
import com.company.agentfoundation.agent.AgentCapabilityResponse;
|
||||
import com.company.agentfoundation.agent.AgentIntent;
|
||||
import com.company.agentfoundation.agent.AgentRequestContext;
|
||||
import com.company.agentfoundation.agent.report.dto.AgentCapabilityResponse;
|
||||
import com.company.agentfoundation.agent.report.model.AgentIntent;
|
||||
import com.company.agentfoundation.agent.report.model.AgentRequestContext;
|
||||
|
||||
public interface AgentCapabilityHandler {
|
||||
AgentIntent intent();
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
package com.company.agentfoundation.agent.capability;
|
||||
|
||||
import com.company.agentfoundation.agent.AgentCapabilityResponse;
|
||||
import com.company.agentfoundation.agent.AgentIntent;
|
||||
import com.company.agentfoundation.agent.AgentRequestContext;
|
||||
import com.company.agentfoundation.agent.report.dto.AgentCapabilityResponse;
|
||||
import com.company.agentfoundation.agent.report.model.AgentIntent;
|
||||
import com.company.agentfoundation.agent.report.model.AgentRequestContext;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.EnumMap;
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
package com.company.agentfoundation.agent.capability;
|
||||
package com.company.agentfoundation.agent.capability.content;
|
||||
|
||||
import com.company.agentfoundation.agent.AgentIntent;
|
||||
import com.company.agentfoundation.agent.capability.AbstractPlaceholderCapabilityHandler;
|
||||
|
||||
import com.company.agentfoundation.agent.report.model.AgentIntent;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.List;
|
||||
@ -1,10 +1,12 @@
|
||||
package com.company.agentfoundation.agent.capability;
|
||||
package com.company.agentfoundation.agent.capability.customer;
|
||||
|
||||
import com.company.agentfoundation.agent.AgentCapabilityResponse;
|
||||
import com.company.agentfoundation.agent.AgentIntent;
|
||||
import com.company.agentfoundation.agent.AgentRequestContext;
|
||||
import com.company.agentfoundation.integration.wecom.WecomStudentContactStats;
|
||||
import com.company.agentfoundation.integration.wecom.WecomStudentContactStatsService;
|
||||
import com.company.agentfoundation.agent.capability.AgentCapabilityHandler;
|
||||
|
||||
import com.company.agentfoundation.agent.report.dto.AgentCapabilityResponse;
|
||||
import com.company.agentfoundation.agent.report.model.AgentIntent;
|
||||
import com.company.agentfoundation.agent.report.model.AgentRequestContext;
|
||||
import com.company.agentfoundation.integration.wecom.dto.WecomStudentContactStats;
|
||||
import com.company.agentfoundation.integration.wecom.service.WecomStudentContactStatsService;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.time.Instant;
|
||||
@ -1,6 +1,8 @@
|
||||
package com.company.agentfoundation.agent.capability;
|
||||
package com.company.agentfoundation.agent.capability.ec;
|
||||
|
||||
import com.company.agentfoundation.agent.AgentIntent;
|
||||
import com.company.agentfoundation.agent.capability.AbstractPlaceholderCapabilityHandler;
|
||||
|
||||
import com.company.agentfoundation.agent.report.model.AgentIntent;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.List;
|
||||
@ -1,6 +1,8 @@
|
||||
package com.company.agentfoundation.agent.capability;
|
||||
package com.company.agentfoundation.agent.capability.integration;
|
||||
|
||||
import com.company.agentfoundation.agent.AgentIntent;
|
||||
import com.company.agentfoundation.agent.capability.AbstractPlaceholderCapabilityHandler;
|
||||
|
||||
import com.company.agentfoundation.agent.report.model.AgentIntent;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.List;
|
||||
@ -1,6 +1,8 @@
|
||||
package com.company.agentfoundation.agent.capability;
|
||||
package com.company.agentfoundation.agent.capability.management;
|
||||
|
||||
import com.company.agentfoundation.agent.AgentIntent;
|
||||
import com.company.agentfoundation.agent.capability.AbstractPlaceholderCapabilityHandler;
|
||||
|
||||
import com.company.agentfoundation.agent.report.model.AgentIntent;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.List;
|
||||
@ -1,6 +1,8 @@
|
||||
package com.company.agentfoundation.agent.capability;
|
||||
package com.company.agentfoundation.agent.capability.student;
|
||||
|
||||
import com.company.agentfoundation.agent.AgentIntent;
|
||||
import com.company.agentfoundation.agent.capability.AbstractPlaceholderCapabilityHandler;
|
||||
|
||||
import com.company.agentfoundation.agent.report.model.AgentIntent;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.List;
|
||||
@ -1,6 +1,8 @@
|
||||
package com.company.agentfoundation.agent.capability;
|
||||
package com.company.agentfoundation.agent.capability.wecom;
|
||||
|
||||
import com.company.agentfoundation.agent.AgentIntent;
|
||||
import com.company.agentfoundation.agent.capability.AbstractPlaceholderCapabilityHandler;
|
||||
|
||||
import com.company.agentfoundation.agent.report.model.AgentIntent;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.List;
|
||||
@ -1,4 +1,7 @@
|
||||
package com.company.agentfoundation.agent;
|
||||
package com.company.agentfoundation.agent.report.controller;
|
||||
|
||||
import com.company.agentfoundation.agent.report.dto.AgentCapabilityResponse;
|
||||
import com.company.agentfoundation.agent.report.service.OperationReportService;
|
||||
|
||||
import com.company.agentfoundation.agent.capability.AgentCapabilityDescriptor;
|
||||
import com.company.agentfoundation.agent.capability.CapabilityRegistry;
|
||||
@ -1,4 +1,6 @@
|
||||
package com.company.agentfoundation.agent;
|
||||
package com.company.agentfoundation.agent.report.dto;
|
||||
|
||||
import com.company.agentfoundation.agent.report.model.AgentIntent;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
@ -1,4 +1,4 @@
|
||||
package com.company.agentfoundation.agent;
|
||||
package com.company.agentfoundation.agent.report.model;
|
||||
|
||||
public enum AgentIntent {
|
||||
DATA_SYNC_STATUS("data_sync_status", "integration", "\u4e09\u7cfb\u7edf\u6570\u636e\u540c\u6b65\u72b6\u6001"),
|
||||
@ -1,4 +1,4 @@
|
||||
package com.company.agentfoundation.agent;
|
||||
package com.company.agentfoundation.agent.report.model;
|
||||
|
||||
public record AgentRequestContext(
|
||||
String agentId,
|
||||
@ -1,4 +1,8 @@
|
||||
package com.company.agentfoundation.agent;
|
||||
package com.company.agentfoundation.agent.report.service;
|
||||
|
||||
import com.company.agentfoundation.agent.report.dto.AgentCapabilityResponse;
|
||||
import com.company.agentfoundation.agent.report.model.AgentIntent;
|
||||
import com.company.agentfoundation.agent.report.model.AgentRequestContext;
|
||||
|
||||
import com.company.agentfoundation.agent.capability.CapabilityRegistry;
|
||||
import org.springframework.stereotype.Service;
|
||||
@ -1,4 +1,8 @@
|
||||
package com.company.agentfoundation.agent;
|
||||
package com.company.agentfoundation.agent.runtime.controller;
|
||||
|
||||
import com.company.agentfoundation.agent.runtime.dto.AgentMessageRequest;
|
||||
import com.company.agentfoundation.agent.runtime.dto.AgentMessageResponse;
|
||||
import com.company.agentfoundation.agent.runtime.service.AgentOrchestratorService;
|
||||
|
||||
import com.company.agentfoundation.security.AgentAuthService;
|
||||
import jakarta.validation.Valid;
|
||||
@ -1,4 +1,4 @@
|
||||
package com.company.agentfoundation.agent;
|
||||
package com.company.agentfoundation.agent.runtime.dto;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
package com.company.agentfoundation.agent;
|
||||
package com.company.agentfoundation.agent.runtime.dto;
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
@ -1,10 +1,15 @@
|
||||
package com.company.agentfoundation.agent;
|
||||
package com.company.agentfoundation.agent.runtime.service;
|
||||
|
||||
import com.company.agentfoundation.domain.ChatRequest;
|
||||
import com.company.agentfoundation.domain.ChatResponse;
|
||||
import com.company.agentfoundation.directory.EmployeeAccountMapping;
|
||||
import com.company.agentfoundation.directory.EmployeeDirectoryResponse;
|
||||
import com.company.agentfoundation.directory.EmployeeDirectoryService;
|
||||
import com.company.agentfoundation.agent.report.dto.AgentCapabilityResponse;
|
||||
import com.company.agentfoundation.agent.report.model.AgentIntent;
|
||||
import com.company.agentfoundation.agent.report.model.AgentRequestContext;
|
||||
import com.company.agentfoundation.agent.report.service.OperationReportService;
|
||||
|
||||
import com.company.agentfoundation.dto.chat.ChatRequest;
|
||||
import com.company.agentfoundation.dto.chat.ChatResponse;
|
||||
import com.company.agentfoundation.directory.dto.EmployeeAccountMapping;
|
||||
import com.company.agentfoundation.directory.dto.EmployeeDirectoryResponse;
|
||||
import com.company.agentfoundation.directory.service.EmployeeDirectoryService;
|
||||
import com.company.agentfoundation.security.AgentContext;
|
||||
import com.company.agentfoundation.service.ChatService;
|
||||
import org.slf4j.Logger;
|
||||
@ -1,7 +1,9 @@
|
||||
package com.company.agentfoundation.agent;
|
||||
package com.company.agentfoundation.agent.runtime.service;
|
||||
|
||||
import com.company.agentfoundation.domain.ChatRequest;
|
||||
import com.company.agentfoundation.domain.ChatResponse;
|
||||
import com.company.agentfoundation.agent.report.model.AgentIntent;
|
||||
|
||||
import com.company.agentfoundation.dto.chat.ChatRequest;
|
||||
import com.company.agentfoundation.dto.chat.ChatResponse;
|
||||
import com.company.agentfoundation.security.AgentContext;
|
||||
import com.company.agentfoundation.service.ChatService;
|
||||
import org.slf4j.Logger;
|
||||
@ -1,4 +1,7 @@
|
||||
package com.company.agentfoundation.api;
|
||||
|
||||
import com.company.agentfoundation.api.dto.DeepseekChatRequest;
|
||||
import com.company.agentfoundation.api.dto.DeepseekChatResponse;
|
||||
import com.company.agentfoundation.config.FoundationProperties;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.MediaType;
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
package com.company.agentfoundation.api;
|
||||
package com.company.agentfoundation.api.dto;
|
||||
import java.util.List;
|
||||
public record DeepseekChatRequest(String model, List<Message> messages, Double temperature, Boolean stream) {
|
||||
public record Message(String role, String content) {}
|
||||
@ -1,4 +1,4 @@
|
||||
package com.company.agentfoundation.api;
|
||||
package com.company.agentfoundation.api.dto;
|
||||
import java.util.List;
|
||||
public record DeepseekChatResponse(List<Choice> choices, Usage usage) {
|
||||
public record Choice(Message message) {}
|
||||
@ -1,6 +1,6 @@
|
||||
package com.company.agentfoundation.controller;
|
||||
import com.company.agentfoundation.domain.ChatRequest;
|
||||
import com.company.agentfoundation.domain.ChatResponse;
|
||||
import com.company.agentfoundation.dto.chat.ChatRequest;
|
||||
import com.company.agentfoundation.dto.chat.ChatResponse;
|
||||
import com.company.agentfoundation.security.AgentAuthService;
|
||||
import com.company.agentfoundation.security.AgentContext;
|
||||
import com.company.agentfoundation.service.ChatService;
|
||||
|
||||
@ -1,4 +1,6 @@
|
||||
package com.company.agentfoundation.crm;
|
||||
package com.company.agentfoundation.crm.controller;
|
||||
|
||||
import com.company.agentfoundation.crm.service.CrmDatabaseService;
|
||||
|
||||
import com.company.agentfoundation.security.AgentAuthService;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
@ -1,4 +1,4 @@
|
||||
package com.company.agentfoundation.crm;
|
||||
package com.company.agentfoundation.crm.service;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
@ -1,4 +1,4 @@
|
||||
package com.company.agentfoundation.directory;
|
||||
package com.company.agentfoundation.directory.dto;
|
||||
|
||||
public record EmployeeAccountMapping(
|
||||
Long id,
|
||||
@ -1,4 +1,4 @@
|
||||
package com.company.agentfoundation.directory;
|
||||
package com.company.agentfoundation.directory.dto;
|
||||
|
||||
public record EmployeeDataPermission(
|
||||
String sourceSystem,
|
||||
@ -1,4 +1,4 @@
|
||||
package com.company.agentfoundation.directory;
|
||||
package com.company.agentfoundation.directory.dto;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@ -1,4 +1,8 @@
|
||||
package com.company.agentfoundation.directory;
|
||||
package com.company.agentfoundation.directory.service;
|
||||
|
||||
import com.company.agentfoundation.directory.dto.EmployeeAccountMapping;
|
||||
import com.company.agentfoundation.directory.dto.EmployeeDataPermission;
|
||||
import com.company.agentfoundation.directory.dto.EmployeeDirectoryResponse;
|
||||
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
@ -1,4 +1,4 @@
|
||||
package com.company.agentfoundation.domain;
|
||||
package com.company.agentfoundation.dto.chat;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotEmpty;
|
||||
import java.util.List;
|
||||
@ -1,4 +1,4 @@
|
||||
package com.company.agentfoundation.domain;
|
||||
package com.company.agentfoundation.dto.chat;
|
||||
import java.time.Instant;
|
||||
public record ChatResponse(String traceId, String agentId, String model, String content, Usage usage, Instant timestamp) {
|
||||
public record Usage(Integer promptTokens, Integer completionTokens, Integer totalTokens) {}
|
||||
@ -1,4 +1,13 @@
|
||||
package com.company.agentfoundation.integration.wecom;
|
||||
package com.company.agentfoundation.integration.wecom.client;
|
||||
|
||||
import com.company.agentfoundation.integration.wecom.dto.WecomApiResponse;
|
||||
import com.company.agentfoundation.integration.wecom.dto.WecomExternalContactBatchRequest;
|
||||
import com.company.agentfoundation.integration.wecom.dto.WecomExternalContactBatchResponse;
|
||||
import com.company.agentfoundation.integration.wecom.dto.WecomFollowUserListResponse;
|
||||
import com.company.agentfoundation.integration.wecom.dto.WecomSendMessageRequest;
|
||||
import com.company.agentfoundation.integration.wecom.dto.WecomTokenResponse;
|
||||
import com.company.agentfoundation.integration.wecom.dto.WecomUserBehaviorDataRequest;
|
||||
import com.company.agentfoundation.integration.wecom.dto.WecomUserBehaviorDataResponse;
|
||||
|
||||
import com.company.agentfoundation.config.FoundationProperties;
|
||||
import org.slf4j.Logger;
|
||||
@ -1,6 +1,11 @@
|
||||
package com.company.agentfoundation.integration.wecom;
|
||||
package com.company.agentfoundation.integration.wecom.controller;
|
||||
|
||||
import com.company.agentfoundation.agent.AgentOrchestratorService;
|
||||
import com.company.agentfoundation.integration.wecom.client.WecomClient;
|
||||
import com.company.agentfoundation.integration.wecom.model.WecomEncryptedEnvelope;
|
||||
import com.company.agentfoundation.integration.wecom.model.WecomIncomingMessage;
|
||||
import com.company.agentfoundation.integration.wecom.service.WecomCryptoService;
|
||||
|
||||
import com.company.agentfoundation.agent.runtime.service.AgentOrchestratorService;
|
||||
import com.company.agentfoundation.config.FoundationProperties;
|
||||
import com.fasterxml.jackson.dataformat.xml.XmlMapper;
|
||||
import org.slf4j.Logger;
|
||||
@ -1,4 +1,4 @@
|
||||
package com.company.agentfoundation.integration.wecom;
|
||||
package com.company.agentfoundation.integration.wecom.dto;
|
||||
|
||||
public record WecomApiResponse(Integer errcode, String errmsg) {
|
||||
public boolean success() {
|
||||
@ -1,4 +1,4 @@
|
||||
package com.company.agentfoundation.integration.wecom;
|
||||
package com.company.agentfoundation.integration.wecom.dto;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
package com.company.agentfoundation.integration.wecom;
|
||||
package com.company.agentfoundation.integration.wecom.dto;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
package com.company.agentfoundation.integration.wecom;
|
||||
package com.company.agentfoundation.integration.wecom.dto;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
package com.company.agentfoundation.integration.wecom;
|
||||
package com.company.agentfoundation.integration.wecom.dto;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
package com.company.agentfoundation.integration.wecom;
|
||||
package com.company.agentfoundation.integration.wecom.dto;
|
||||
|
||||
public record WecomStudentContactStats(
|
||||
int totalStudents,
|
||||
@ -1,4 +1,4 @@
|
||||
package com.company.agentfoundation.integration.wecom;
|
||||
package com.company.agentfoundation.integration.wecom.dto;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
package com.company.agentfoundation.integration.wecom;
|
||||
package com.company.agentfoundation.integration.wecom.dto;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
package com.company.agentfoundation.integration.wecom;
|
||||
package com.company.agentfoundation.integration.wecom.dto;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
package com.company.agentfoundation.integration.wecom;
|
||||
package com.company.agentfoundation.integration.wecom.model;
|
||||
|
||||
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
|
||||
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement;
|
||||
@ -1,4 +1,4 @@
|
||||
package com.company.agentfoundation.integration.wecom;
|
||||
package com.company.agentfoundation.integration.wecom.model;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
|
||||
@ -1,4 +1,4 @@
|
||||
package com.company.agentfoundation.integration.wecom;
|
||||
package com.company.agentfoundation.integration.wecom.service;
|
||||
|
||||
import com.company.agentfoundation.config.FoundationProperties;
|
||||
import org.springframework.stereotype.Service;
|
||||
@ -1,4 +1,9 @@
|
||||
package com.company.agentfoundation.integration.wecom;
|
||||
package com.company.agentfoundation.integration.wecom.service;
|
||||
|
||||
import com.company.agentfoundation.integration.wecom.client.WecomClient;
|
||||
import com.company.agentfoundation.integration.wecom.dto.WecomExternalContactBatchResponse;
|
||||
import com.company.agentfoundation.integration.wecom.dto.WecomStudentContactStats;
|
||||
import com.company.agentfoundation.integration.wecom.dto.WecomUserBehaviorDataResponse;
|
||||
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@ -0,0 +1,71 @@
|
||||
package com.company.agentfoundation.media.controller;
|
||||
|
||||
import com.company.agentfoundation.media.dto.CreateMediaTaskRequest;
|
||||
import com.company.agentfoundation.media.dto.MediaTaskListResponse;
|
||||
import com.company.agentfoundation.media.dto.MediaTaskResponse;
|
||||
import com.company.agentfoundation.media.service.MediaTaskService;
|
||||
import com.company.agentfoundation.security.AgentAuthService;
|
||||
import jakarta.validation.Valid;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestHeader;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/v1/media")
|
||||
public class MediaTaskController {
|
||||
private final AgentAuthService authService;
|
||||
private final MediaTaskService mediaTaskService;
|
||||
|
||||
public MediaTaskController(AgentAuthService authService, MediaTaskService mediaTaskService) {
|
||||
this.authService = authService;
|
||||
this.mediaTaskService = mediaTaskService;
|
||||
}
|
||||
|
||||
@PostMapping("/tasks")
|
||||
public ResponseEntity<MediaTaskResponse> createTask(
|
||||
@RequestHeader("X-Agent-Id") String agentId,
|
||||
@RequestHeader("X-Internal-Key") String internalKey,
|
||||
@Valid @RequestBody CreateMediaTaskRequest request
|
||||
) {
|
||||
authService.authenticate(agentId, internalKey);
|
||||
return ResponseEntity.ok(mediaTaskService.create(request));
|
||||
}
|
||||
|
||||
@GetMapping("/tasks")
|
||||
public ResponseEntity<MediaTaskListResponse> listTasks(
|
||||
@RequestHeader("X-Agent-Id") String agentId,
|
||||
@RequestHeader("X-Internal-Key") String internalKey,
|
||||
@RequestParam(value = "status", required = false) String status,
|
||||
@RequestParam(value = "platform", required = false) String platform
|
||||
) {
|
||||
authService.authenticate(agentId, internalKey);
|
||||
return ResponseEntity.ok(MediaTaskListResponse.from(mediaTaskService.list(status, platform)));
|
||||
}
|
||||
|
||||
@GetMapping("/tasks/{taskId}")
|
||||
public ResponseEntity<MediaTaskResponse> getTask(
|
||||
@RequestHeader("X-Agent-Id") String agentId,
|
||||
@RequestHeader("X-Internal-Key") String internalKey,
|
||||
@PathVariable String taskId
|
||||
) {
|
||||
authService.authenticate(agentId, internalKey);
|
||||
return ResponseEntity.ok(mediaTaskService.get(taskId));
|
||||
}
|
||||
|
||||
@GetMapping("/dashboard/summary")
|
||||
public ResponseEntity<Map<String, Object>> dashboardSummary(
|
||||
@RequestHeader("X-Agent-Id") String agentId,
|
||||
@RequestHeader("X-Internal-Key") String internalKey
|
||||
) {
|
||||
authService.authenticate(agentId, internalKey);
|
||||
return ResponseEntity.ok(mediaTaskService.dashboardSummary());
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,53 @@
|
||||
package com.company.agentfoundation.media.controller;
|
||||
|
||||
import com.company.agentfoundation.media.dto.MediaTaskResponse;
|
||||
import com.company.agentfoundation.media.dto.WorkerPollRequest;
|
||||
import com.company.agentfoundation.media.dto.WorkerTaskResultRequest;
|
||||
import com.company.agentfoundation.media.service.MediaTaskService;
|
||||
import jakarta.validation.Valid;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestHeader;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/v1/media/workers")
|
||||
public class MediaWorkerController {
|
||||
private static final String LOCAL_WORKER_TOKEN = "local-worker-dev-token";
|
||||
|
||||
private final MediaTaskService mediaTaskService;
|
||||
|
||||
public MediaWorkerController(MediaTaskService mediaTaskService) {
|
||||
this.mediaTaskService = mediaTaskService;
|
||||
}
|
||||
|
||||
@PostMapping("/poll")
|
||||
public ResponseEntity<List<MediaTaskResponse>> poll(
|
||||
@RequestHeader("X-Worker-Token") String workerToken,
|
||||
@Valid @RequestBody WorkerPollRequest request
|
||||
) {
|
||||
authenticateWorker(workerToken);
|
||||
return ResponseEntity.ok(mediaTaskService.poll(request));
|
||||
}
|
||||
|
||||
@PostMapping("/tasks/{taskId}/result")
|
||||
public ResponseEntity<MediaTaskResponse> result(
|
||||
@RequestHeader("X-Worker-Token") String workerToken,
|
||||
@PathVariable String taskId,
|
||||
@Valid @RequestBody WorkerTaskResultRequest request
|
||||
) {
|
||||
authenticateWorker(workerToken);
|
||||
return ResponseEntity.ok(mediaTaskService.complete(taskId, request));
|
||||
}
|
||||
|
||||
private void authenticateWorker(String workerToken) {
|
||||
if (!LOCAL_WORKER_TOKEN.equals(workerToken)) {
|
||||
throw new IllegalArgumentException("Invalid local worker token");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,15 @@
|
||||
package com.company.agentfoundation.media.dto;
|
||||
|
||||
import com.company.agentfoundation.media.model.MediaTaskType;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
public record CreateMediaTaskRequest(
|
||||
@NotNull MediaTaskType taskType,
|
||||
@NotBlank String platform,
|
||||
@NotBlank String accountId,
|
||||
Map<String, Object> payload
|
||||
) {
|
||||
}
|
||||
@ -0,0 +1,12 @@
|
||||
package com.company.agentfoundation.media.dto;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public record MediaTaskListResponse(
|
||||
int total,
|
||||
List<MediaTaskResponse> items
|
||||
) {
|
||||
public static MediaTaskListResponse from(List<MediaTaskResponse> items) {
|
||||
return new MediaTaskListResponse(items.size(), items);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,38 @@
|
||||
package com.company.agentfoundation.media.dto;
|
||||
|
||||
import com.company.agentfoundation.media.model.MediaTask;
|
||||
import com.company.agentfoundation.media.model.MediaTaskStatus;
|
||||
import com.company.agentfoundation.media.model.MediaTaskType;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.Map;
|
||||
|
||||
public record MediaTaskResponse(
|
||||
String id,
|
||||
MediaTaskType taskType,
|
||||
String platform,
|
||||
String accountId,
|
||||
MediaTaskStatus status,
|
||||
String assignedWorkerId,
|
||||
Map<String, Object> payload,
|
||||
Map<String, Object> result,
|
||||
String errorMessage,
|
||||
Instant createdAt,
|
||||
Instant updatedAt
|
||||
) {
|
||||
public static MediaTaskResponse from(MediaTask task) {
|
||||
return new MediaTaskResponse(
|
||||
task.getId(),
|
||||
task.getTaskType(),
|
||||
task.getPlatform(),
|
||||
task.getAccountId(),
|
||||
task.getStatus(),
|
||||
task.getAssignedWorkerId(),
|
||||
task.getPayload(),
|
||||
task.getResult(),
|
||||
task.getErrorMessage(),
|
||||
task.getCreatedAt(),
|
||||
task.getUpdatedAt()
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,10 @@
|
||||
package com.company.agentfoundation.media.dto;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public record WorkerPollRequest(
|
||||
String workerId,
|
||||
List<String> platforms,
|
||||
int maxTasks
|
||||
) {
|
||||
}
|
||||
@ -0,0 +1,12 @@
|
||||
package com.company.agentfoundation.media.dto;
|
||||
|
||||
import com.company.agentfoundation.media.model.MediaTaskStatus;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
public record WorkerTaskResultRequest(
|
||||
MediaTaskStatus status,
|
||||
Map<String, Object> result,
|
||||
String errorMessage
|
||||
) {
|
||||
}
|
||||
@ -0,0 +1,162 @@
|
||||
package com.company.agentfoundation.media.model;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public class MediaTask {
|
||||
private final String id;
|
||||
private final MediaTaskType taskType;
|
||||
private final String platform;
|
||||
private final String accountId;
|
||||
private final Map<String, Object> payload;
|
||||
private final Instant createdAt;
|
||||
private MediaTaskStatus status;
|
||||
private String assignedWorkerId;
|
||||
private Map<String, Object> result;
|
||||
private String errorMessage;
|
||||
private Instant updatedAt;
|
||||
|
||||
public MediaTask(String id, MediaTaskType taskType, String platform, String accountId, Map<String, Object> payload) {
|
||||
this.id = id;
|
||||
this.taskType = taskType;
|
||||
this.platform = platform;
|
||||
this.accountId = accountId;
|
||||
this.payload = payload == null ? new LinkedHashMap<>() : new LinkedHashMap<>(payload);
|
||||
this.createdAt = Instant.now();
|
||||
this.updatedAt = createdAt;
|
||||
this.status = MediaTaskStatus.PENDING;
|
||||
this.result = new LinkedHashMap<>();
|
||||
}
|
||||
|
||||
public String getId() { return id; }
|
||||
public MediaTaskType getTaskType() { return taskType; }
|
||||
public String getPlatform() { return platform; }
|
||||
public String getAccountId() { return accountId; }
|
||||
public Map<String, Object> getPayload() { return payload; }
|
||||
public Instant getCreatedAt() { return createdAt; }
|
||||
public MediaTaskStatus getStatus() { return status; }
|
||||
public String getAssignedWorkerId() { return assignedWorkerId; }
|
||||
public Map<String, Object> getResult() { return result; }
|
||||
public String getErrorMessage() { return errorMessage; }
|
||||
public Instant getUpdatedAt() { return updatedAt; }
|
||||
|
||||
public void assignTo(String workerId) {
|
||||
this.assignedWorkerId = workerId;
|
||||
this.status = MediaTaskStatus.RUNNING;
|
||||
this.updatedAt = Instant.now();
|
||||
}
|
||||
|
||||
public void complete(MediaTaskStatus status, Map<String, Object> result, String errorMessage) {
|
||||
if (status != MediaTaskStatus.SUCCESS && status != MediaTaskStatus.FAILED) {
|
||||
throw new IllegalArgumentException("Task result status must be SUCCESS or FAILED");
|
||||
}
|
||||
this.status = status;
|
||||
this.result = result == null ? new LinkedHashMap<>() : new LinkedHashMap<>(result);
|
||||
this.errorMessage = errorMessage;
|
||||
this.updatedAt = Instant.now();
|
||||
}
|
||||
|
||||
public static Map<String, Object> aggregateMetrics(List<MediaTask> tasks) {
|
||||
long views = 0;
|
||||
long likes = 0;
|
||||
long comments = 0;
|
||||
long collects = 0;
|
||||
long shares = 0;
|
||||
List<Map<String, Object>> rows = new ArrayList<>();
|
||||
List<Map<String, Object>> videoRows = new ArrayList<>();
|
||||
for (MediaTask task : tasks) {
|
||||
if (task.status != MediaTaskStatus.SUCCESS || task.result == null) {
|
||||
continue;
|
||||
}
|
||||
Map<String, Object> 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<String, Object> 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"))
|
||||
));
|
||||
}
|
||||
}
|
||||
Map<String, Object> summary = new LinkedHashMap<>();
|
||||
summary.put("views", views);
|
||||
summary.put("likes", likes);
|
||||
summary.put("comments", comments);
|
||||
summary.put("collects", collects);
|
||||
summary.put("shares", shares);
|
||||
summary.put("interactionCount", likes + comments + collects + shares);
|
||||
summary.put("interactionRate", views <= 0 ? 0.0 : (likes + comments + collects + shares) * 1.0 / views);
|
||||
summary.put("rows", rows);
|
||||
summary.put("videoCount", videoRows.size());
|
||||
summary.put("videoRows", videoRows);
|
||||
return summary;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private static Map<String, Object> mapValue(Object value) {
|
||||
if (value instanceof Map<?, ?> map) {
|
||||
return (Map<String, Object>) map;
|
||||
}
|
||||
return Map.of();
|
||||
}
|
||||
|
||||
private static long longValue(Object value) {
|
||||
if (value instanceof Number number) {
|
||||
return number.longValue();
|
||||
}
|
||||
if (value instanceof String text && !text.isBlank()) {
|
||||
return Long.parseLong(text);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private static List<Map<String, Object>> listOfMaps(Object value) {
|
||||
if (!(value instanceof List<?> list)) {
|
||||
return List.of();
|
||||
}
|
||||
List<Map<String, Object>> maps = new ArrayList<>();
|
||||
for (Object item : list) {
|
||||
if (item instanceof Map<?, ?> map) {
|
||||
maps.add((Map<String, Object>) map);
|
||||
}
|
||||
}
|
||||
return maps;
|
||||
}
|
||||
|
||||
private static String stringValue(Object value) {
|
||||
return value == null ? "" : String.valueOf(value);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,8 @@
|
||||
package com.company.agentfoundation.media.model;
|
||||
|
||||
public enum MediaTaskStatus {
|
||||
PENDING,
|
||||
RUNNING,
|
||||
SUCCESS,
|
||||
FAILED
|
||||
}
|
||||
@ -0,0 +1,8 @@
|
||||
package com.company.agentfoundation.media.model;
|
||||
|
||||
public enum MediaTaskType {
|
||||
SYNC_COMMENTS,
|
||||
SYNC_METRICS,
|
||||
REPLY_COMMENT,
|
||||
PUBLISH_CONTENT
|
||||
}
|
||||
@ -0,0 +1,118 @@
|
||||
package com.company.agentfoundation.media.service;
|
||||
|
||||
import com.company.agentfoundation.media.dto.CreateMediaTaskRequest;
|
||||
import com.company.agentfoundation.media.dto.MediaTaskResponse;
|
||||
import com.company.agentfoundation.media.dto.WorkerPollRequest;
|
||||
import com.company.agentfoundation.media.dto.WorkerTaskResultRequest;
|
||||
import com.company.agentfoundation.media.model.MediaTask;
|
||||
import com.company.agentfoundation.media.model.MediaTaskStatus;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
@Service
|
||||
public class MediaTaskService {
|
||||
private final Map<String, MediaTask> tasks = new ConcurrentHashMap<>();
|
||||
private final Map<String, Instant> workerHeartbeats = new ConcurrentHashMap<>();
|
||||
|
||||
public MediaTaskResponse create(CreateMediaTaskRequest request) {
|
||||
MediaTask task = new MediaTask(
|
||||
"media_" + UUID.randomUUID(),
|
||||
request.taskType(),
|
||||
normalize(request.platform()),
|
||||
request.accountId(),
|
||||
request.payload()
|
||||
);
|
||||
tasks.put(task.getId(), task);
|
||||
return MediaTaskResponse.from(task);
|
||||
}
|
||||
|
||||
public MediaTaskResponse get(String taskId) {
|
||||
return MediaTaskResponse.from(required(taskId));
|
||||
}
|
||||
|
||||
public List<MediaTaskResponse> list(String status, String platform) {
|
||||
return tasks.values().stream()
|
||||
.filter(task -> status == null || status.isBlank() || task.getStatus().name().equalsIgnoreCase(status))
|
||||
.filter(task -> platform == null || platform.isBlank() || task.getPlatform().equalsIgnoreCase(platform))
|
||||
.sorted(Comparator.comparing(MediaTask::getCreatedAt).reversed())
|
||||
.map(MediaTaskResponse::from)
|
||||
.toList();
|
||||
}
|
||||
|
||||
public List<MediaTaskResponse> poll(WorkerPollRequest request) {
|
||||
String workerId = request.workerId() == null || request.workerId().isBlank()
|
||||
? "local-worker"
|
||||
: request.workerId();
|
||||
int limit = request.maxTasks() <= 0 ? 1 : Math.min(request.maxTasks(), 10);
|
||||
List<String> platforms = request.platforms() == null
|
||||
? List.of()
|
||||
: request.platforms().stream().map(this::normalize).toList();
|
||||
workerHeartbeats.put(workerId, Instant.now());
|
||||
|
||||
List<MediaTaskResponse> assigned = new ArrayList<>();
|
||||
for (MediaTask task : tasks.values().stream()
|
||||
.filter(task -> task.getStatus() == MediaTaskStatus.PENDING)
|
||||
.filter(task -> platforms.isEmpty() || platforms.contains(task.getPlatform()))
|
||||
.sorted(Comparator.comparing(MediaTask::getCreatedAt))
|
||||
.toList()) {
|
||||
synchronized (task) {
|
||||
if (task.getStatus() != MediaTaskStatus.PENDING) {
|
||||
continue;
|
||||
}
|
||||
task.assignTo(workerId);
|
||||
assigned.add(MediaTaskResponse.from(task));
|
||||
}
|
||||
if (assigned.size() >= limit) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return assigned;
|
||||
}
|
||||
|
||||
public MediaTaskResponse complete(String taskId, WorkerTaskResultRequest request) {
|
||||
MediaTask task = required(taskId);
|
||||
MediaTaskStatus status = request.status() == null ? MediaTaskStatus.SUCCESS : request.status();
|
||||
synchronized (task) {
|
||||
task.complete(status, request.result(), request.errorMessage());
|
||||
}
|
||||
return MediaTaskResponse.from(task);
|
||||
}
|
||||
|
||||
public Map<String, Object> dashboardSummary() {
|
||||
List<MediaTask> allTasks = new ArrayList<>(tasks.values());
|
||||
Map<String, Object> summary = new LinkedHashMap<>();
|
||||
summary.put("taskCount", allTasks.size());
|
||||
summary.put("pendingCount", countByStatus(allTasks, MediaTaskStatus.PENDING));
|
||||
summary.put("runningCount", countByStatus(allTasks, MediaTaskStatus.RUNNING));
|
||||
summary.put("successCount", countByStatus(allTasks, MediaTaskStatus.SUCCESS));
|
||||
summary.put("failedCount", countByStatus(allTasks, MediaTaskStatus.FAILED));
|
||||
summary.put("metrics", MediaTask.aggregateMetrics(allTasks));
|
||||
summary.put("workers", workerHeartbeats);
|
||||
return summary;
|
||||
}
|
||||
|
||||
private long countByStatus(List<MediaTask> allTasks, MediaTaskStatus status) {
|
||||
return allTasks.stream().filter(task -> task.getStatus() == status).count();
|
||||
}
|
||||
|
||||
private MediaTask required(String taskId) {
|
||||
MediaTask task = tasks.get(taskId);
|
||||
if (task == null) {
|
||||
throw new IllegalArgumentException("Media task not found: " + taskId);
|
||||
}
|
||||
return task;
|
||||
}
|
||||
|
||||
private String normalize(String platform) {
|
||||
return platform == null ? "" : platform.trim().toLowerCase(Locale.ROOT);
|
||||
}
|
||||
}
|
||||
@ -1,9 +1,9 @@
|
||||
package com.company.agentfoundation.controller;
|
||||
package com.company.agentfoundation.miniprogram.jumprope.controller;
|
||||
|
||||
import com.company.agentfoundation.miniprogram.jumprope.JumpRopeAiResponse;
|
||||
import com.company.agentfoundation.miniprogram.jumprope.JumpRopeAiService;
|
||||
import com.company.agentfoundation.miniprogram.jumprope.JumpRopeAnalyzeRequest;
|
||||
import com.company.agentfoundation.miniprogram.jumprope.JumpRopeChatRequest;
|
||||
import com.company.agentfoundation.miniprogram.jumprope.dto.JumpRopeAiResponse;
|
||||
import com.company.agentfoundation.miniprogram.jumprope.service.JumpRopeAiService;
|
||||
import com.company.agentfoundation.miniprogram.jumprope.dto.JumpRopeAnalyzeRequest;
|
||||
import com.company.agentfoundation.miniprogram.jumprope.dto.JumpRopeChatRequest;
|
||||
import jakarta.validation.Valid;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
@ -1,4 +1,6 @@
|
||||
package com.company.agentfoundation.miniprogram.jumprope;
|
||||
package com.company.agentfoundation.miniprogram.jumprope.dto;
|
||||
|
||||
import com.company.agentfoundation.miniprogram.jumprope.model.JumpRopeVideoRecommendation;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
package com.company.agentfoundation.miniprogram.jumprope;
|
||||
package com.company.agentfoundation.miniprogram.jumprope.dto;
|
||||
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
package com.company.agentfoundation.miniprogram.jumprope;
|
||||
package com.company.agentfoundation.miniprogram.jumprope.dto;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
package com.company.agentfoundation.miniprogram.jumprope;
|
||||
package com.company.agentfoundation.miniprogram.jumprope.model;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
package com.company.agentfoundation.miniprogram.jumprope;
|
||||
package com.company.agentfoundation.miniprogram.jumprope.model;
|
||||
|
||||
public record JumpRopeMessage(
|
||||
Long id,
|
||||
@ -1,4 +1,4 @@
|
||||
package com.company.agentfoundation.miniprogram.jumprope;
|
||||
package com.company.agentfoundation.miniprogram.jumprope.model;
|
||||
|
||||
public record JumpRopeSession(
|
||||
Long id,
|
||||
@ -1,4 +1,4 @@
|
||||
package com.company.agentfoundation.miniprogram.jumprope;
|
||||
package com.company.agentfoundation.miniprogram.jumprope.model;
|
||||
|
||||
public record JumpRopeVideoRecommendation(
|
||||
Integer resourceId,
|
||||
@ -1,4 +1,4 @@
|
||||
package com.company.agentfoundation.miniprogram.jumprope;
|
||||
package com.company.agentfoundation.miniprogram.jumprope.model;
|
||||
|
||||
public record MiniProgramUser(
|
||||
Integer userId,
|
||||
@ -1,4 +1,10 @@
|
||||
package com.company.agentfoundation.miniprogram.jumprope;
|
||||
package com.company.agentfoundation.miniprogram.jumprope.repository;
|
||||
|
||||
import com.company.agentfoundation.miniprogram.jumprope.model.JumpRopeExerciseData;
|
||||
import com.company.agentfoundation.miniprogram.jumprope.model.JumpRopeMessage;
|
||||
import com.company.agentfoundation.miniprogram.jumprope.model.JumpRopeSession;
|
||||
import com.company.agentfoundation.miniprogram.jumprope.model.JumpRopeVideoRecommendation;
|
||||
import com.company.agentfoundation.miniprogram.jumprope.model.MiniProgramUser;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
@ -1,7 +1,17 @@
|
||||
package com.company.agentfoundation.miniprogram.jumprope;
|
||||
package com.company.agentfoundation.miniprogram.jumprope.service;
|
||||
|
||||
import com.company.agentfoundation.domain.ChatRequest;
|
||||
import com.company.agentfoundation.domain.ChatResponse;
|
||||
import com.company.agentfoundation.miniprogram.jumprope.dto.JumpRopeAiResponse;
|
||||
import com.company.agentfoundation.miniprogram.jumprope.dto.JumpRopeAnalyzeRequest;
|
||||
import com.company.agentfoundation.miniprogram.jumprope.dto.JumpRopeChatRequest;
|
||||
import com.company.agentfoundation.miniprogram.jumprope.model.JumpRopeExerciseData;
|
||||
import com.company.agentfoundation.miniprogram.jumprope.model.JumpRopeMessage;
|
||||
import com.company.agentfoundation.miniprogram.jumprope.model.JumpRopeSession;
|
||||
import com.company.agentfoundation.miniprogram.jumprope.model.JumpRopeVideoRecommendation;
|
||||
import com.company.agentfoundation.miniprogram.jumprope.model.MiniProgramUser;
|
||||
import com.company.agentfoundation.miniprogram.jumprope.repository.JumpRopeAiRepository;
|
||||
|
||||
import com.company.agentfoundation.dto.chat.ChatRequest;
|
||||
import com.company.agentfoundation.dto.chat.ChatResponse;
|
||||
import com.company.agentfoundation.security.AgentContext;
|
||||
import com.company.agentfoundation.service.ChatService;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
@ -1,9 +1,9 @@
|
||||
package com.company.agentfoundation.service;
|
||||
import com.company.agentfoundation.api.DeepseekChatRequest;
|
||||
import com.company.agentfoundation.api.DeepseekChatResponse;
|
||||
import com.company.agentfoundation.api.dto.DeepseekChatRequest;
|
||||
import com.company.agentfoundation.api.dto.DeepseekChatResponse;
|
||||
import com.company.agentfoundation.api.DeepseekClient;
|
||||
import com.company.agentfoundation.domain.ChatRequest;
|
||||
import com.company.agentfoundation.domain.ChatResponse;
|
||||
import com.company.agentfoundation.dto.chat.ChatRequest;
|
||||
import com.company.agentfoundation.dto.chat.ChatResponse;
|
||||
import com.company.agentfoundation.security.AgentContext;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
5
start_local_client.bat
Normal file
5
start_local_client.bat
Normal file
@ -0,0 +1,5 @@
|
||||
@echo off
|
||||
setlocal
|
||||
cd /d "%~dp0"
|
||||
powershell.exe -NoProfile -ExecutionPolicy Bypass -File "%~dp0scripts\start_local_client.ps1"
|
||||
pause
|
||||
5
start_yuedong_client.bat
Normal file
5
start_yuedong_client.bat
Normal file
@ -0,0 +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
|
||||
pause
|
||||
Loading…
x
Reference in New Issue
Block a user