88 lines
2.8 KiB
JavaScript
88 lines
2.8 KiB
JavaScript
const EMPTY_RANKING = Object.freeze({
|
|
candidateCount: 0,
|
|
eventCount: 0,
|
|
events: [],
|
|
selected: [],
|
|
});
|
|
|
|
export function emptyNewsEngineRanking() {
|
|
return {
|
|
candidateCount: 0,
|
|
eventCount: 0,
|
|
events: [],
|
|
selected: [],
|
|
};
|
|
}
|
|
|
|
export function resolveNewsEngineEndpoint(env = process.env) {
|
|
return String(env.MEMIND_NEWS_ENGINE_URL ?? '').trim().replace(/\/+$/, '');
|
|
}
|
|
|
|
export async function rankNewsWithEngine(groups, {
|
|
env = process.env,
|
|
now = Date.now(),
|
|
fetchImpl = fetch,
|
|
logger = console,
|
|
} = {}) {
|
|
const endpoint = resolveNewsEngineEndpoint(env);
|
|
if (!endpoint) return emptyNewsEngineRanking();
|
|
|
|
const timeoutMs = Math.max(
|
|
500,
|
|
Number(env.MEMIND_NEWS_ENGINE_TIMEOUT_MS ?? 5000) || 5000,
|
|
);
|
|
const controller = new AbortController();
|
|
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
timer.unref?.();
|
|
try {
|
|
const token = String(env.MEMIND_NEWS_ENGINE_API_TOKEN ?? '').trim();
|
|
const response = await fetchImpl(`${endpoint}/v1/rank`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'content-type': 'application/json',
|
|
...(token ? { authorization: `Bearer ${token}` } : {}),
|
|
},
|
|
body: JSON.stringify({
|
|
groups: Array.isArray(groups) ? groups : [],
|
|
options: {
|
|
now,
|
|
limit: Number(env.MEMIND_NEWS_ENGINE_DAILY_LIMIT ?? 15) || 15,
|
|
minimumScore: Number(env.MEMIND_NEWS_ENGINE_MINIMUM_SCORE ?? 0) || 0,
|
|
},
|
|
}),
|
|
signal: controller.signal,
|
|
});
|
|
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
const ranking = await response.json();
|
|
if (!ranking || !Array.isArray(ranking.events) || !Array.isArray(ranking.selected)) {
|
|
throw new Error('invalid ranking response');
|
|
}
|
|
return ranking;
|
|
} catch (error) {
|
|
logger.warn?.(
|
|
'[NewsEngine] ranking unavailable:',
|
|
error instanceof Error ? error.message : error,
|
|
);
|
|
return { ...EMPTY_RANKING, events: [], selected: [] };
|
|
} finally {
|
|
clearTimeout(timer);
|
|
}
|
|
}
|
|
|
|
export function formatRankedNewsCandidates(ranking) {
|
|
const selected = Array.isArray(ranking?.selected) ? ranking.selected : [];
|
|
if (!selected.length) return '';
|
|
const lines = [
|
|
'【News Engine 编辑候选】以下事件已完成聚类、规则评分和栏目配额选择。生成早报时优先采用;must_include=true 的事件必须进入正文。',
|
|
];
|
|
for (const event of selected) {
|
|
const source = event.primaryArticle?.sourceDomain || 'unknown';
|
|
lines.push(
|
|
`${event.rank}. [${event.category}] ${event.canonicalTitle}`,
|
|
` score=${event.finalScore} sources=${event.sourceCount} must_include=${event.mustInclude} primary=${source}`,
|
|
);
|
|
if (event.primaryArticle?.url) lines.push(` ${event.primaryArticle.url}`);
|
|
}
|
|
return lines.join('\n');
|
|
}
|