Merge PR #48: feat: add Xiaohongshu source + Reddit public fallback
- Xiaohongshu search via local MCP service (opt-in, zero impact if service not running) - Reddit public JSON fallback (works with zero API keys) - Reddit priority: ScrapeCreators -> OpenAI -> public fallback - Updated env.py: Reddit always available via public fallback Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Vendored
+131
@@ -0,0 +1,131 @@
|
||||
import { TwitterClient } from '../lib/twitter-client.js';
|
||||
function formatPostCount(count) {
|
||||
if (count >= 1_000_000) {
|
||||
return `${(count / 1_000_000).toFixed(1)}M`;
|
||||
}
|
||||
if (count >= 1_000) {
|
||||
return `${(count / 1_000).toFixed(1)}K`;
|
||||
}
|
||||
return String(count);
|
||||
}
|
||||
function printNewsItems(items, ctx, opts = {}) {
|
||||
if (opts.json) {
|
||||
console.log(JSON.stringify(items, null, 2));
|
||||
return;
|
||||
}
|
||||
if (items.length === 0) {
|
||||
console.log(opts.emptyMessage ?? 'No news items found.');
|
||||
return;
|
||||
}
|
||||
for (const item of items) {
|
||||
const categoryLabel = item.category ? `[${item.category}]` : '';
|
||||
console.log(`\n${ctx.colors.accent(categoryLabel)} ${ctx.colors.command(item.headline)}`);
|
||||
if (item.description) {
|
||||
console.log(` ${ctx.colors.muted(item.description)}`);
|
||||
}
|
||||
const meta = [];
|
||||
if (item.timeAgo) {
|
||||
meta.push(item.timeAgo);
|
||||
}
|
||||
if (item.postCount) {
|
||||
meta.push(`${formatPostCount(item.postCount)} posts`);
|
||||
}
|
||||
if (meta.length > 0) {
|
||||
console.log(` ${ctx.colors.muted(meta.join(' | '))}`);
|
||||
}
|
||||
if (item.url) {
|
||||
console.log(` ${ctx.l('url')}${item.url}`);
|
||||
}
|
||||
// Print related tweets if available
|
||||
if (item.tweets && item.tweets.length > 0) {
|
||||
console.log(` ${ctx.colors.section('Related tweets:')}`);
|
||||
const tweetLimit = opts.tweetLimit ?? item.tweets.length;
|
||||
for (const tweet of item.tweets.slice(0, tweetLimit)) {
|
||||
console.log(` @${tweet.author.username}: ${tweet.text.slice(0, 100)}${tweet.text.length > 100 ? '...' : ''}`);
|
||||
}
|
||||
}
|
||||
console.log(ctx.colors.muted('─'.repeat(50)));
|
||||
}
|
||||
}
|
||||
export function registerNewsCommand(program, ctx) {
|
||||
program
|
||||
.command('news')
|
||||
.alias('trending')
|
||||
.description('Fetch AI-curated news and trending topics from Explore tabs')
|
||||
.option('-n, --count <number>', 'Number of items to fetch', '10')
|
||||
.option('--ai-only', 'Show only AI-curated news items')
|
||||
.option('--with-tweets', 'Also fetch related tweets for each news item')
|
||||
.option('--tweets-per-item <number>', 'Number of tweets to fetch per news item (default: 5)', '5')
|
||||
.option('--for-you', 'Fetch only from For You tab')
|
||||
.option('--news-only', 'Fetch only from News tab')
|
||||
.option('--sports', 'Fetch only from Sports tab')
|
||||
.option('--entertainment', 'Fetch only from Entertainment tab')
|
||||
.option('--trending-only', 'Fetch only from Trending tab')
|
||||
.option('--json', 'Output as JSON')
|
||||
.option('--json-full', 'Output as JSON with full raw API response in _raw field')
|
||||
.action(async (cmdOpts) => {
|
||||
const opts = program.opts();
|
||||
const timeoutMs = ctx.resolveTimeoutFromOptions(opts);
|
||||
const quoteDepth = ctx.resolveQuoteDepthFromOptions(opts);
|
||||
const count = Number.parseInt(cmdOpts.count || '10', 10);
|
||||
const tweetsPerItem = Number.parseInt(cmdOpts.tweetsPerItem || '5', 10);
|
||||
const { cookies, warnings } = await ctx.resolveCredentialsFromOptions(opts);
|
||||
for (const warning of warnings) {
|
||||
console.error(`${ctx.p('warn')}${warning}`);
|
||||
}
|
||||
if (Number.isNaN(count) || count < 1) {
|
||||
console.error(`${ctx.p('err')}--count must be a positive number`);
|
||||
process.exit(1);
|
||||
}
|
||||
if (Number.isNaN(tweetsPerItem) || tweetsPerItem < 1) {
|
||||
console.error(`${ctx.p('err')}--tweets-per-item must be a positive number`);
|
||||
process.exit(1);
|
||||
}
|
||||
if (!cookies.authToken || !cookies.ct0) {
|
||||
console.error(`${ctx.p('err')}Missing required credentials`);
|
||||
process.exit(1);
|
||||
}
|
||||
// Determine which tabs to fetch from
|
||||
const tabs = [];
|
||||
if (cmdOpts.forYou) {
|
||||
tabs.push('forYou');
|
||||
}
|
||||
if (cmdOpts.newsOnly) {
|
||||
tabs.push('news');
|
||||
}
|
||||
if (cmdOpts.sports) {
|
||||
tabs.push('sports');
|
||||
}
|
||||
if (cmdOpts.entertainment) {
|
||||
tabs.push('entertainment');
|
||||
}
|
||||
if (cmdOpts.trendingOnly) {
|
||||
tabs.push('trending');
|
||||
}
|
||||
// If no specific tabs selected, use defaults (all tabs except trending)
|
||||
const tabsToFetch = tabs.length > 0 ? tabs : undefined;
|
||||
const client = new TwitterClient({ cookies, timeoutMs, quoteDepth });
|
||||
const includeRaw = cmdOpts.jsonFull ?? false;
|
||||
const withTweets = cmdOpts.withTweets ?? false;
|
||||
const aiOnly = cmdOpts.aiOnly ?? false;
|
||||
const result = await client.getNews(count, {
|
||||
includeRaw,
|
||||
withTweets,
|
||||
tweetsPerItem,
|
||||
aiOnly,
|
||||
tabs: tabsToFetch,
|
||||
});
|
||||
if (result.success) {
|
||||
printNewsItems(result.items, ctx, {
|
||||
json: cmdOpts.json || cmdOpts.jsonFull,
|
||||
emptyMessage: 'No news items found.',
|
||||
tweetLimit: withTweets ? tweetsPerItem : undefined,
|
||||
});
|
||||
}
|
||||
else {
|
||||
console.error(`${ctx.p('err')}Failed to fetch news: ${result.error}`);
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
}
|
||||
//# sourceMappingURL=news.js.map
|
||||
Reference in New Issue
Block a user