feat: 趋势英文内容自动翻译为中文+JSON截断容错解析

This commit is contained in:
Yuzhiran Dev
2026-07-12 20:07:44 +08:00
parent 89cf7add0e
commit 1094435b5f
+127
View File
@@ -128,6 +128,128 @@ async function fetchArxiv() {
}).filter(p => p.title && p.title.length > 10 && new Date(p.newsDate).getTime() > weekAgo);
}
/** 检测字符串是否包含中文 */
function hasChinese(text) {
return /[\u4e00-\u9fff]/.test(text);
}
/** 调用 AI 模型批量翻译英文标题和摘要到中文 */
async function translateItems(items) {
const needTranslate = items.filter(i => !hasChinese(i.title));
if (needTranslate.length === 0) return items;
const apiUrl = process.env.OPENAI_API_URL || 'https://opencode.ai/zen/v1/chat/completions';
const apiKey = ''; // OpenCode Zen 无需 API Key
const model = process.env.OPENAI_MODEL || 'deepseek-v4-flash-free';
console.log(` → 翻译 ${needTranslate.length} 条英文内容...`);
// 分批翻译,每次最多 4 条
const BATCH = 4;
for (let i = 0; i < needTranslate.length; i += BATCH) {
const batch = needTranslate.slice(i, i + BATCH);
const input = batch.map((item, idx) => {
return `[${idx + 1}] Title: ${item.title}\nSummary: ${item.summary}`;
}).join('\n\n');
const prompt = `You are an AI news translator. Translate the following AI-related news headlines to Chinese.
Rules:
- Keep technical terms/model names/company names in English (e.g., GPT-4, arXiv, NVIDIA, GitHub, Qwen, Grok)
- Keep proper nouns like "Show HN:", "Mindwalk" untranslated
- Output ONLY a JSON array in this exact format, no other text:
[
{"title": "translated title 1", "summary": "translated summary 1"},
{"title": "translated title 2", "summary": "translated summary 2"}
]
News to translate:
${input}`;
try {
const result = await httpPost(apiUrl, {
model,
messages: [{ role: 'user', content: prompt }],
temperature: 0.1,
max_tokens: 2048
}, apiKey);
const text = result?.choices?.[0]?.message?.content || '';
console.log(` ↳ 翻译响应(前100): ${text.slice(0, 100)}`);
// 尝试提取 JSON - 用贪婪匹配(] 只在数组末尾出现)
const jsonMatch = text.match(/\[[\s\S]*\]/);
if (jsonMatch) {
// 尝试修复常见 JSON 截断问题
let jsonStr = jsonMatch[0];
try {
const translations = JSON.parse(jsonStr);
if (Array.isArray(translations)) {
translations.forEach((t, idx) => {
if (idx < batch.length && t.title) {
batch[idx].title = t.title;
if (t.summary) batch[idx].summary = t.summary;
}
});
}
} catch (parseErr) {
// 截断了,尝试逐行模式:按标题-摘要配对解析
console.log(` ↳ JSON 截断,尝试文本解析...`);
// 从响应中提取所有 title: 和 summary: 字段
const titles = [...jsonStr.matchAll(/"title"\s*:\s*"((?:[^"\\]|\\.)*)"/g)].map(m => m[1]);
const summaries = [...jsonStr.matchAll(/"summary"\s*:\s*"((?:[^"\\]|\\.)*)"/g)].map(m => m[1]);
titles.forEach((t, idx) => {
if (idx < batch.length && !hasChinese(batch[idx].title)) {
batch[idx].title = t;
if (summaries[idx]) batch[idx].summary = summaries[idx];
}
});
}
} else {
console.log(` ⚠ 批次 ${i / BATCH + 1} 解析失败: ${text.slice(0, 100)}`);
}
} catch (e) {
console.log(` ⚠ 翻译批次 ${i / BATCH + 1} 失败: ${e.message}`);
}
// 等一秒避免限流
if (i + BATCH < needTranslate.length) {
await new Promise(r => setTimeout(r, 1000));
}
}
return items;
}
/** HTTP POST (JSON) */
function httpPost(url, body, apiKey) {
return new Promise((resolve, reject) => {
const data = JSON.stringify(body);
const u = new URL(url);
const options = {
hostname: u.hostname, port: u.port || 443, path: u.pathname + u.search,
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Content-Length': Buffer.byteLength(data)
},
timeout: 30000
};
if (apiKey) options.headers['Authorization'] = `Bearer ${apiKey}`;
const req = https.request(options, (res) => {
let d = '';
res.on('data', c => d += c);
res.on('end', () => {
try { resolve(JSON.parse(d)); }
catch (e) { reject(new Error('parse failed: ' + d.slice(0, 100))); }
});
});
req.on('error', reject);
req.on('timeout', function () { req.destroy(); reject(new Error('timeout')); });
req.write(data);
req.end();
});
}
/** 保底数据(高质量中文 AI 新闻) */
const FALLBACK = [
{ title: 'DeepSeek 秘密造芯:推理芯片项目已启动一年', summary: '据路透社报道,DeepSeek 正在自研推理芯片,旨在降低对英伟达的依赖,项目已启动超过一年。', source: '路透社', sourceUrl: 'https://reuters.com', category: '公司', hot: true, newsDate: () => new Date() },
@@ -173,6 +295,11 @@ async function main() {
console.log(` ✓ 保底: ${need}`);
}
// 翻译英文内容为中文
if (items.length > 0) {
items = await translateItems(items);
}
// 清理旧数据
const cutoff = new Date(Date.now() - MAX_AGE_DAYS * 86400000);
const deleted = await Trend.deleteMany({ newsDate: { $lt: cutoff } });