feat: News region (EU, Russia, China, America), auto-geo через GeoJS
- Настройка News sources region в Settings → Search - Источники новостей по регионам (SOURCES_BY_REGION) - Авто: geo-context + fallback GeoJS по IP (без geo-device) - Discover: ожидание региона перед fetch, исправлен race - API: Promise.allSettled для устойчивости при сбоях SearxNG - Язык поиска: ru/zh/en по региону Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,30 +1,141 @@
|
||||
import { searchSearxng } from '@/lib/searxng';
|
||||
import configManager from '@/lib/config';
|
||||
import { getSearxngURL } from '@/lib/config/serverRegistry';
|
||||
|
||||
const websitesForTopic = {
|
||||
tech: {
|
||||
query: ['technology news', 'latest tech', 'AI', 'science and innovation'],
|
||||
links: ['techcrunch.com', 'wired.com', 'theverge.com'],
|
||||
type Region = 'america' | 'eu' | 'russia' | 'china';
|
||||
type Topic = 'tech' | 'finance' | 'art' | 'sports' | 'entertainment';
|
||||
|
||||
const SOURCES_BY_REGION: Record<
|
||||
Region,
|
||||
Record<Topic, { query: string[]; links: string[] }>
|
||||
> = {
|
||||
america: {
|
||||
tech: {
|
||||
query: ['technology news', 'latest tech', 'AI', 'science and innovation'],
|
||||
links: ['techcrunch.com', 'wired.com', 'theverge.com', 'arstechnica.com'],
|
||||
},
|
||||
finance: {
|
||||
query: ['finance news', 'economy', 'stock market', 'investing'],
|
||||
links: ['bloomberg.com', 'cnbc.com', 'marketwatch.com', 'reuters.com'],
|
||||
},
|
||||
art: {
|
||||
query: ['art news', 'culture', 'modern art', 'cultural events'],
|
||||
links: ['artnews.com', 'hyperallergic.com', 'theartnewspaper.com'],
|
||||
},
|
||||
sports: {
|
||||
query: ['sports news', 'latest sports', 'cricket football tennis'],
|
||||
links: ['espn.com', 'cbssports.com', 'nfl.com', 'nba.com'],
|
||||
},
|
||||
entertainment: {
|
||||
query: ['entertainment news', 'movies', 'TV shows', 'celebrities'],
|
||||
links: ['hollywoodreporter.com', 'variety.com', 'deadline.com'],
|
||||
},
|
||||
},
|
||||
finance: {
|
||||
query: ['finance news', 'economy', 'stock market', 'investing'],
|
||||
links: ['bloomberg.com', 'cnbc.com', 'marketwatch.com'],
|
||||
eu: {
|
||||
tech: {
|
||||
query: ['technology news', 'tech', 'AI', 'innovation'],
|
||||
links: ['techcrunch.com', 'bbc.com', 'theguardian.com', 'reuters.com', 'euronews.com'],
|
||||
},
|
||||
finance: {
|
||||
query: ['finance news', 'economy', 'stock market', 'euro'],
|
||||
links: ['reuters.com', 'ft.com', 'bbc.com', 'euronews.com', 'politico.eu'],
|
||||
},
|
||||
art: {
|
||||
query: ['art news', 'culture', 'art', 'exhibition'],
|
||||
links: ['theguardian.com', 'bbc.com', 'dw.com', 'france24.com'],
|
||||
},
|
||||
sports: {
|
||||
query: ['sports news', 'football', 'football soccer', 'champions league'],
|
||||
links: ['bbc.com/sport', 'skysports.com', 'uefa.com', 'eurosport.com'],
|
||||
},
|
||||
entertainment: {
|
||||
query: ['entertainment news', 'films', 'music', 'culture'],
|
||||
links: ['bbc.com', 'theguardian.com', 'euronews.com', 'dw.com'],
|
||||
},
|
||||
},
|
||||
art: {
|
||||
query: ['art news', 'culture', 'modern art', 'cultural events'],
|
||||
links: ['artnews.com', 'hyperallergic.com', 'theartnewspaper.com'],
|
||||
russia: {
|
||||
tech: {
|
||||
query: ['technology news', 'tech', 'IT', 'innovation'],
|
||||
links: ['tass.com', 'ria.ru', 'interfax.com', 'kommersant.ru', 'vedomosti.ru'],
|
||||
},
|
||||
finance: {
|
||||
query: ['finance news', 'economy', 'markets', 'ruble'],
|
||||
links: ['tass.com', 'ria.ru', 'interfax.com', 'kommersant.ru', 'vedomosti.ru'],
|
||||
},
|
||||
art: {
|
||||
query: ['art news', 'culture', 'exhibition', 'museum'],
|
||||
links: ['tass.com', 'ria.ru', 'kommersant.ru', 'interfax.com'],
|
||||
},
|
||||
sports: {
|
||||
query: ['sports news', 'football', 'hockey', 'olympics'],
|
||||
links: ['tass.com', 'ria.ru', 'rsport.ria.ru', 'championat.com', 'sport-express.ru'],
|
||||
},
|
||||
entertainment: {
|
||||
query: ['entertainment news', 'films', 'music', 'culture'],
|
||||
links: ['tass.com', 'ria.ru', 'interfax.com', 'kommersant.ru'],
|
||||
},
|
||||
},
|
||||
sports: {
|
||||
query: ['sports news', 'latest sports', 'cricket football tennis'],
|
||||
links: ['espn.com', 'bbc.com/sport', 'skysports.com'],
|
||||
},
|
||||
entertainment: {
|
||||
query: ['entertainment news', 'movies', 'TV shows', 'celebrities'],
|
||||
links: ['hollywoodreporter.com', 'variety.com', 'deadline.com'],
|
||||
china: {
|
||||
tech: {
|
||||
query: ['technology news', 'tech', 'AI', 'innovation'],
|
||||
links: ['scmp.com', 'xinhuanet.com', 'chinadaily.com.cn', 'reuters.com'],
|
||||
},
|
||||
finance: {
|
||||
query: ['finance news', 'economy', 'China markets', 'investing'],
|
||||
links: ['scmp.com', 'chinadaily.com.cn', 'reuters.com', 'bloomberg.com'],
|
||||
},
|
||||
art: {
|
||||
query: ['art news', 'culture', 'exhibition', 'Chinese art'],
|
||||
links: ['scmp.com', 'chinadaily.com.cn', 'xinhuanet.com'],
|
||||
},
|
||||
sports: {
|
||||
query: ['sports news', 'Olympics', 'football', 'basketball'],
|
||||
links: ['scmp.com', 'chinadaily.com.cn', 'xinhuanet.com'],
|
||||
},
|
||||
entertainment: {
|
||||
query: ['entertainment news', 'films', 'music', 'culture'],
|
||||
links: ['scmp.com', 'chinadaily.com.cn', 'variety.com'],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
type Topic = keyof typeof websitesForTopic;
|
||||
const COUNTRY_TO_REGION: Record<string, Region> = {
|
||||
US: 'america',
|
||||
CA: 'america',
|
||||
MX: 'america',
|
||||
RU: 'russia',
|
||||
BY: 'russia',
|
||||
KZ: 'russia',
|
||||
CN: 'china',
|
||||
HK: 'china',
|
||||
TW: 'china',
|
||||
DE: 'eu',
|
||||
FR: 'eu',
|
||||
IT: 'eu',
|
||||
ES: 'eu',
|
||||
GB: 'eu',
|
||||
UK: 'eu',
|
||||
NL: 'eu',
|
||||
PL: 'eu',
|
||||
BE: 'eu',
|
||||
AT: 'eu',
|
||||
PT: 'eu',
|
||||
SE: 'eu',
|
||||
FI: 'eu',
|
||||
IE: 'eu',
|
||||
GR: 'eu',
|
||||
RO: 'eu',
|
||||
CZ: 'eu',
|
||||
HU: 'eu',
|
||||
BG: 'eu',
|
||||
HR: 'eu',
|
||||
SK: 'eu',
|
||||
SI: 'eu',
|
||||
LT: 'eu',
|
||||
LV: 'eu',
|
||||
EE: 'eu',
|
||||
DK: 'eu',
|
||||
};
|
||||
|
||||
export const GET = async (req: Request) => {
|
||||
try {
|
||||
@@ -44,29 +155,61 @@ export const GET = async (req: Request) => {
|
||||
const mode: 'normal' | 'preview' =
|
||||
(params.get('mode') as 'normal' | 'preview') || 'normal';
|
||||
const topic: Topic = (params.get('topic') as Topic) || 'tech';
|
||||
const regionParam = params.get('region') as Region | null;
|
||||
|
||||
const selectedTopic = websitesForTopic[topic];
|
||||
let region: Region = 'america';
|
||||
const configRegion = configManager.getConfig('search.newsRegion', 'auto') as
|
||||
| Region
|
||||
| 'auto';
|
||||
|
||||
if (configRegion !== 'auto' && ['america', 'eu', 'russia', 'china'].includes(configRegion)) {
|
||||
region = configRegion;
|
||||
} else if (regionParam && ['america', 'eu', 'russia', 'china'].includes(regionParam)) {
|
||||
region = regionParam;
|
||||
} else if (configRegion === 'auto') {
|
||||
try {
|
||||
const geoUrl =
|
||||
process.env.GEO_DEVICE_SERVICE_URL ?? 'http://localhost:4002';
|
||||
const geoRes = await fetch(`${geoUrl}/api/context`, {
|
||||
headers: {
|
||||
'x-forwarded-for': req.headers.get('x-forwarded-for') ?? '',
|
||||
'x-real-ip': req.headers.get('x-real-ip') ?? '',
|
||||
'user-agent': req.headers.get('user-agent') ?? '',
|
||||
},
|
||||
});
|
||||
const geoData = await geoRes.json();
|
||||
const cc = geoData?.geo?.countryCode;
|
||||
if (cc && COUNTRY_TO_REGION[cc]) {
|
||||
region = COUNTRY_TO_REGION[cc];
|
||||
}
|
||||
} catch {
|
||||
// keep default america
|
||||
}
|
||||
}
|
||||
|
||||
const selectedTopic = SOURCES_BY_REGION[region][topic];
|
||||
|
||||
const searchLang = region === 'russia' ? 'ru' : region === 'china' ? 'zh' : 'en';
|
||||
let data = [];
|
||||
|
||||
if (mode === 'normal') {
|
||||
const seenUrls = new Set();
|
||||
|
||||
data = (
|
||||
await Promise.all(
|
||||
selectedTopic.links.flatMap((link) =>
|
||||
selectedTopic.query.map(async (query) => {
|
||||
return (
|
||||
await searchSearxng(`site:${link} ${query}`, {
|
||||
engines: ['bing news'],
|
||||
pageno: 1,
|
||||
language: 'en',
|
||||
})
|
||||
).results;
|
||||
}),
|
||||
),
|
||||
)
|
||||
)
|
||||
const searchPromises = selectedTopic.links.flatMap((link) =>
|
||||
selectedTopic.query.map((query) =>
|
||||
searchSearxng(`site:${link} ${query}`, {
|
||||
engines: ['bing news'],
|
||||
pageno: 1,
|
||||
language: searchLang,
|
||||
}).then((r) => r.results),
|
||||
),
|
||||
);
|
||||
const settled = await Promise.allSettled(searchPromises);
|
||||
const allResults = settled
|
||||
.filter((r): r is PromiseFulfilledResult<{ url?: string; title?: string }[]> => r.status === 'fulfilled')
|
||||
.flatMap((r) => r.value);
|
||||
|
||||
data = allResults
|
||||
.flat()
|
||||
.filter((item) => {
|
||||
const url = item.url?.toLowerCase().trim();
|
||||
@@ -82,7 +225,7 @@ export const GET = async (req: Request) => {
|
||||
{
|
||||
engines: ['bing news'],
|
||||
pageno: 1,
|
||||
language: 'en',
|
||||
language: searchLang,
|
||||
},
|
||||
)
|
||||
).results;
|
||||
|
||||
Reference in New Issue
Block a user