From 1094435b5fabd7ff1b2c13f1ac211780e64dc369 Mon Sep 17 00:00:00 2001 From: Yuzhiran Dev Date: Sun, 12 Jul 2026 20:07:44 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E8=B6=8B=E5=8A=BF=E8=8B=B1=E6=96=87?= =?UTF-8?q?=E5=86=85=E5=AE=B9=E8=87=AA=E5=8A=A8=E7=BF=BB=E8=AF=91=E4=B8=BA?= =?UTF-8?q?=E4=B8=AD=E6=96=87+JSON=E6=88=AA=E6=96=AD=E5=AE=B9=E9=94=99?= =?UTF-8?q?=E8=A7=A3=E6=9E=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/src/scripts/update-trends.js | 127 ++++++++++++++++++++++++++++ 1 file changed, 127 insertions(+) diff --git a/server/src/scripts/update-trends.js b/server/src/scripts/update-trends.js index 3b2c406..7491639 100644 --- a/server/src/scripts/update-trends.js +++ b/server/src/scripts/update-trends.js @@ -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 } });