9b395f5cc3
E2E Headed Chrome / e2e-headed (macos-15) (push) Has been cancelled
E2E Headed Chrome / e2e-headed (ubuntu-latest) (push) Has been cancelled
E2E Headed Chrome / e2e-headed (windows-latest) (push) Has been cancelled
CI / build (macos-latest) (push) Has been cancelled
CI / build (ubuntu-latest) (push) Has been cancelled
CI / build (windows-latest) (push) Has been cancelled
CI / unit-test (push) Has been cancelled
CI / bun-test (push) Has been cancelled
CI / adapter-test (push) Has been cancelled
CI / smoke-test (macos-latest) (push) Has been cancelled
CI / smoke-test (ubuntu-latest) (push) Has been cancelled
Security Audit / audit (push) Has been cancelled
Build Chrome Extension / build (push) Has been cancelled
Trigger Website Rebuild (Docs Updated) / dispatch (push) Has been cancelled
111 lines
4.0 KiB
JavaScript
111 lines
4.0 KiB
JavaScript
import { cli, Strategy } from '@jackwener/opencli/registry';
|
|
import { ArgumentError, CommandExecutionError, EmptyResultError, getErrorMessage } from '@jackwener/opencli/errors';
|
|
|
|
const AIBASE_DAILY_URL = 'https://www.aibase.com/zh/daily';
|
|
const DEFAULT_LIMIT = 20;
|
|
const MAX_LIMIT = 50;
|
|
|
|
function normalizeLimit(value) {
|
|
const raw = value ?? DEFAULT_LIMIT;
|
|
const limit = Number(raw);
|
|
if (!Number.isInteger(limit) || limit <= 0) {
|
|
throw new ArgumentError('limit must be a positive integer', `Example: opencli aibase news --limit ${DEFAULT_LIMIT}`);
|
|
}
|
|
if (limit > MAX_LIMIT) {
|
|
throw new ArgumentError(`limit must be <= ${MAX_LIMIT}`, `Example: opencli aibase news --limit ${MAX_LIMIT}`);
|
|
}
|
|
return limit;
|
|
}
|
|
|
|
function normalizeText(value) {
|
|
return String(value ?? '').replace(/\s+/g, ' ').trim();
|
|
}
|
|
|
|
function buildExtractAibaseNewsJs() {
|
|
return `
|
|
(() => {
|
|
const anchors = Array.from(document.querySelectorAll('.bg-white .grid a[href], a[href*="/zh/daily/"]'))
|
|
.filter((anchor) => {
|
|
const href = anchor.getAttribute('href') || '';
|
|
const text = (anchor.innerText || anchor.textContent || '').trim();
|
|
return text && href && !href.endsWith('/zh/daily') && !href.endsWith('/zh/daily/');
|
|
});
|
|
if (anchors.length === 0) {
|
|
return {
|
|
ok: false,
|
|
reason: 'selector-missing',
|
|
title: document.title || '',
|
|
bodyText: (document.body?.innerText || document.body?.textContent || '').slice(0, 500),
|
|
};
|
|
}
|
|
const seen = new Set();
|
|
const rows = [];
|
|
for (const anchor of anchors) {
|
|
const url = new URL(anchor.getAttribute('href'), location.href).href;
|
|
if (seen.has(url)) continue;
|
|
seen.add(url);
|
|
rows.push({
|
|
rank: rows.length + 1,
|
|
title: anchor.innerText || anchor.textContent || '',
|
|
url,
|
|
});
|
|
}
|
|
return { ok: true, rows };
|
|
})()
|
|
`;
|
|
}
|
|
|
|
function toRows(payload, limit) {
|
|
if (!payload || typeof payload !== 'object') {
|
|
throw new CommandExecutionError('AIbase daily page returned an unreadable payload');
|
|
}
|
|
if (!payload.ok) {
|
|
const reason = typeof payload.reason === 'string' && payload.reason.trim() ? payload.reason.trim() : 'selector-drift';
|
|
throw new CommandExecutionError(
|
|
`AIbase daily selector drift: ${reason}`,
|
|
payload.title ? `Page title: ${payload.title}` : undefined,
|
|
);
|
|
}
|
|
const rows = (Array.isArray(payload.rows) ? payload.rows : [])
|
|
.map((row, index) => ({
|
|
rank: index + 1,
|
|
title: normalizeText(row.title),
|
|
url: normalizeText(row.url),
|
|
}))
|
|
.filter((row) => row.title && row.url);
|
|
if (rows.length === 0) {
|
|
throw new EmptyResultError('aibase news', 'AIbase daily page loaded, but no article rows with title and URL were extracted.');
|
|
}
|
|
return rows.slice(0, limit).map((row, index) => ({ ...row, rank: index + 1 }));
|
|
}
|
|
|
|
async function loadAibaseNews(page, args) {
|
|
const limit = normalizeLimit(args.limit);
|
|
await page.goto(AIBASE_DAILY_URL, { waitUntil: 'load', settleMs: 3000 });
|
|
const payload = await page.evaluate(buildExtractAibaseNewsJs()).catch((error) => {
|
|
throw new CommandExecutionError(`Failed to extract AIbase daily news: ${getErrorMessage(error)}`);
|
|
});
|
|
return toRows(payload, limit);
|
|
}
|
|
|
|
export const aibaseNewsCommand = cli({
|
|
site: 'aibase',
|
|
name: 'news',
|
|
access: 'read',
|
|
description: 'AIbase 日报 - 每天三分钟关注AI行业趋势',
|
|
domain: 'www.aibase.com',
|
|
strategy: Strategy.PUBLIC,
|
|
browser: true,
|
|
args: [
|
|
{ name: 'limit', type: 'int', default: DEFAULT_LIMIT, help: `Number of news items to return (max ${MAX_LIMIT})` },
|
|
],
|
|
columns: ['rank', 'title', 'url'],
|
|
func: loadAibaseNews,
|
|
});
|
|
|
|
export const __test__ = {
|
|
buildExtractAibaseNewsJs,
|
|
normalizeLimit,
|
|
toRows,
|
|
};
|