Files
memind/news-engine-client.mjs
T
2026-09-22 10:19:54 +08:00

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');
}