Compare commits

..

4 Commits

3 changed files with 357 additions and 55 deletions
+37 -1
View File
@@ -55,6 +55,13 @@
<rich-text :nodes="newsBody"></rich-text> <rich-text :nodes="newsBody"></rich-text>
</view> </view>
<!-- 查看原文 -->
<view class="source-link" v-if="news.sourceUrl" @click="openSource">
<text class="source-link-icon">🔗</text>
<text class="source-link-text">查看原文 · {{ news.source }}</text>
<text class="source-link-arrow"></text>
</view>
<!-- 相关话题 --> <!-- 相关话题 -->
<view class="related-tags" v-if="relatedTags.length"> <view class="related-tags" v-if="relatedTags.length">
<text class="related-label">相关话题</text> <text class="related-label">相关话题</text>
@@ -123,7 +130,7 @@ function formatDate(dateStr) {
const newsBody = computed(() => { const newsBody = computed(() => {
const content = news.value.body || '' const content = news.value.body || ''
if (!content) { if (!content) {
return `<p>${news.value.summary || ''}</p><p style="margin-top:16px;color:rgba(255,255,255,0.35)">完整内容暂未提供,将持续更新...</p>` return `<p style="line-height:1.8;font-size:15px;color:var(--text-primary)">${news.value.summary || '暂无详细内容'}</p>`
} }
return content return content
}) })
@@ -173,6 +180,20 @@ function onShare() {
}) })
} }
function openSource() {
if (!news.value.sourceUrl) return
uni.setClipboardData({
data: news.value.sourceUrl,
success: () => {
uni.showModal({
title: '打开原文',
content: `链接已复制: ${news.value.sourceUrl}\n请在浏览器中打开`,
showCancel: false
})
}
})
}
onMounted(() => { onMounted(() => {
loadDetail() loadDetail()
}) })
@@ -309,6 +330,21 @@ onMounted(() => {
color: var(--text-secondary); color: var(--text-secondary);
} }
// 查看原文
.source-link {
margin: 0 20px 20px;
padding: 14px 16px;
background: var(--glass-bg);
border: 1px solid var(--glass-border);
border-radius: 12px;
display: flex;
align-items: center;
gap: 10px;
}
.source-link-icon { font-size: 14px; }
.source-link-text { flex: 1; font-size: 13px; color: var(--dim1-color); font-weight: 500; }
.source-link-arrow { font-size: 14px; color: var(--text-muted); }
// Related tags // Related tags
.related-tags { .related-tags {
margin: 0 20px 20px; margin: 0 20px 20px;
+1 -1
View File
@@ -87,7 +87,7 @@ const displayItems = computed(() => {
async function fetchTrends() { async function fetchTrends() {
loading.value = true loading.value = true
try { try {
const data = await trendApi.list({ limit: 20 }) const data = await trendApi.list({ limit: 30 })
items.value = data.list || data.items || [] items.value = data.list || data.items || []
} catch (err) { } catch (err) {
console.error('获取趋势失败:', err) console.error('获取趋势失败:', err)
+320 -54
View File
@@ -1,78 +1,344 @@
#!/www/server/nodejs/v24.14.0/bin/node
/** /**
* 趋势数据更新脚本(兼容 Hermes cron 调用) * AI 趋势数据更新脚本 —— 真实数据版
* 从 AI 新闻源获取最新趋势并写入数据库 * 数据源:
* - Hacker News: 精确匹配 AI 关键词(模型名/工具名/论文名)
* - arXiv: 最新 cs.AI/cs.CL 论文(7天内)
* - 保底: 高质量中文 AI 新闻(网络不通时用)
* *
* 用法: node src/scripts/update-trends.js * 用法: node src/scripts/update-trends.js
* 或由 Hermes cron 每周调用 * 定时: 每日 8:00、20:00
*/ */
require('dotenv').config(); require('dotenv').config({ path: __dirname + '/../../.env' });
const mongoose = require('mongoose'); const mongoose = require('mongoose');
const Trend = require('../models/Trend'); const https = require('https');
const DB_URI = process.env.MONGODB_URI || 'mongodb://127.0.0.1:27017/wdkj'; const DB_URI = process.env.MONGODB_URI || 'mongodb://127.0.0.1:27017/wdkj';
const MAX_ITEMS = 30;
const MAX_AGE_DAYS = 7;
// 示例新闻数据(生产环境可从 RSS/API 获取 // 精确 AI 关键词(只匹配这些才采信,避免噪音
const sampleTrends = [ const AI_KEYWORDS = [
{ 'gpt-4', 'gpt4', 'gpt-5', 'gpt5', 'o1', 'o3', 'claude', 'sonnet', 'opus', 'haiku',
title: 'DeepSeek 秘密造芯:推理芯片项目已启动一年', 'gemini', 'llama', 'mistral', 'deepseek', 'qwen', 'yi-', 'glm', 'ernie',
summary: '据报道,DeepSeek 正在自研推理芯片,旨在降低对英伟达的依赖。', 'chatgpt', 'copilot', 'cursor', 'windsurf', 'bolt.new', 'lovable', 'v0.dev',
source: '路透社', sourceUrl: 'https://reuters.com', 'llm', 'large language model', 'foundation model', 'diffusion model',
category: '公司', tags: ['DeepSeek', '芯片'], hot: true, newsDate: new Date() 'transformer', 'attention', 'rag', 'agent', 'mcp', 'a2a', 'function calling',
}, 'sora', 'veo', 'gen-3', 'pika', 'runway', 'midjourney', 'stable diffusion', 'dall-e',
{ 'whisper', 'voice mode', 'speech to speech',
title: '蚂蚁灵波开源 LingBot-World 2.0,世界模型小时级实时生成', 'neural network', 'deep learning', 'reinforcement learning',
summary: 'LingBot-World 2.0 实现了世界模型的小时级实时生成,是具身智能的重要突破。', 'openai', 'anthropic', 'deepmind', 'google ai', 'meta ai', 'xai', 'grok',
source: '量子位', sourceUrl: 'https://qbitai.com', 'nvidia', 'h100', 'b200', 'gb200', 'inference chip',
category: '产品', tags: ['蚂蚁灵波', '世界模型'], newsDate: new Date() 'ai safety', 'alignment', 'superalignment',
}, 'hugging face', 'langchain', 'llamaindex', 'autogpt',
{ 'vectordb', 'pinecone', 'chromadb', 'weaviate',
title: '阿里获 ACL 2026 最佳资源论文奖,揭示 Agent 结构性缺陷', 'fine-tuning', 'sft', 'rlhf', 'dpo', 'grpo',
summary: '阿里研究团队获得 ACL 2026 最佳资源论文奖,深入分析了当前 Agent 系统的结构性缺陷。', 'moe', 'mixture of experts', 'speculative decoding',
source: '机器之心', sourceUrl: 'https://jiqizhixin.com', 'tokenizer', 'embedding', 'semantic kernel',
category: '论文', tags: ['阿里', 'ACL', 'Agent'], newsDate: new Date() 'coding agent', 'devai', 'devin', 'codex',
}, 'world model', 'embodied ai', 'robotics',
{ 'arvix', 'neurips', 'icml', 'iclr', 'acl', 'emnlp', 'cvpr', 'eccv'
title: '腾讯混元 Hy3 正式上线,Agent 任务解决率跃升至 90%', ];
summary: '腾讯混元大模型 Hy3 版本正式上线,在 Agent 任务评测中解决率提升至 90%。',
source: '腾讯云', sourceUrl: 'https://cloud.tencent.com', // HN 高分阈值
category: '产品', tags: ['腾讯', '混元', 'Agent'], newsDate: new Date() const HN_SCORE_MIN = 15;
},
{ // 类别映射
title: 'WAIC 2026 倒计时:7月17日上海开幕,1100+企业参展', function categorize(title) {
summary: '2026 世界人工智能大会即将在上海开幕,将有超过 1100 家企业参展。', const lc = title.toLowerCase();
source: '量子位', sourceUrl: 'https://qbitai.com', if (lc.match(/paper|research|study|benchmark|survey|arxiv|neurips|icml|iclr|acl/)) return '论文';
category: '产品', tags: ['WAIC', '会议'], picked: true, newsDate: new Date() if (lc.match(/github|release|launch|open.?source|tool|framework|library|cli|sdk|api|v0\.|v1\.|v2\.|v3\.|v4\./)) return '工具';
}, if (lc.match(/funding|acqui|microsoft|google|meta|openai|anthropi|nvidia|ipo|layoff|million|billion|startup/)) return '公司';
{ return '产品';
title: 'Browser Use CLI 3.0 发布:体积缩小 6 倍,Token 消耗大幅降低',
summary: 'Browser Use 工具 CLI 3.0 版本发布,体积缩小 6 倍,Token 消耗显著降低。',
source: 'GitHub', sourceUrl: 'https://github.com',
category: '工具', tags: ['Browser Use', '开源'], newsDate: new Date()
} }
function getSource(url, from) {
if (!url) return from || 'AI 资讯';
if (url.includes('github.com')) return 'GitHub';
if (url.includes('arxiv.org')) return 'arXiv';
if (url.includes('huggingface.co')) return 'HuggingFace';
return from || url.replace(/https?:\/\//, '').split('/')[0];
}
function httpGet(url) {
return new Promise((resolve, reject) => {
https.get(url, { timeout: 12000 }, (res) => {
let data = '';
res.on('data', c => data += c);
res.on('end', () => {
try { resolve(JSON.parse(data)); }
catch (e) { resolve(data); }
});
}).on('error', reject).on('timeout', function () { this.destroy(); reject(new Error('timeout')); });
});
}
/** 从 Hacker News 获取 AI 相关热门(精确匹配) */
async function fetchHN() {
const ids = await httpGet('https://hacker-news.firebaseio.com/v0/topstories.json');
const batch = ids.slice(0, 120);
const results = await Promise.allSettled(
batch.map(id => httpGet(`https://hacker-news.firebaseio.com/v0/item/${id}.json`))
);
const items = [];
for (const r of results) {
if (r.status !== 'fulfilled' || !r.value?.title) continue;
const s = r.value;
if (s.score < HN_SCORE_MIN) continue;
const text = (s.title + ' ' + (s.url || '') + ' ' + (s.text || '')).toLowerCase();
if (!AI_KEYWORDS.some(k => text.includes(k))) continue;
// 要求高分或明确AI关联
const matchCount = AI_KEYWORDS.filter(k => text.includes(k)).length;
if (matchCount < 1) continue;
if (matchCount < 2 && s.score < 30 && !text.match(/arxiv|openai|anthropic|deepseek|gpt|claude|llama|qwen|glm|grok/)) continue;
items.push({
title: s.title,
summary: (s.text || '').replace(/<[^>]+>/g, '').slice(0, 200) || s.title,
source: getSource(s.url, 'Hacker News'),
sourceUrl: s.url || `https://news.ycombinator.com/item?id=${s.id}`,
category: categorize(s.title),
tags: [],
hot: s.score > 80,
picked: s.score > 150,
newsDate: new Date(s.time * 1000),
status: 'published'
});
}
return items;
}
/** 从 arXiv 获取最新 AI/CL 论文 */
async function fetchArxiv() {
const args = 'search_query=(cat:cs.AI+OR+cat:cs.CL)&sortBy=submittedDate&sortOrder=descending&max_results=10';
const xml = await httpGet('https://export.arxiv.org/api/query?' + args);
const entries = xml.match(/<entry>[\s\S]*?<\/entry>/g) || [];
const weekAgo = Date.now() - 7 * 86400000;
return entries.slice(0, 8).map(entry => {
const title = (entry.match(/<title>([\s\S]*?)<\/title>/) || ['', ''])[1].replace(/\s+/g, ' ').trim().replace(/^Title:\s*/i, '');
const summary = (entry.match(/<summary>([\s\S]*?)<\/summary>/) || ['', ''])[1].replace(/\s+/g, ' ').trim().slice(0, 200);
const link = (entry.match(/<id>([\s\S]*?)<\/id>/) || ['', ''])[1].trim();
const pub = (entry.match(/<published>([\s\S]*?)<\/published>/) || ['', ''])[1];
const date = new Date(pub);
return {
title, summary: summary || title,
source: 'arXiv', sourceUrl: link,
category: '论文', tags: [], hot: date.getTime() > Date.now() - 2 * 86400000,
newsDate: date, status: 'published'
};
}).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() },
{ title: 'WAIC 2026 倒计时:7月17日上海开幕,1100+企业参展', summary: '2026 世界人工智能大会即将在上海开幕,将有超过 1100 家企业参展。', source: '量子位', sourceUrl: 'https://qbitai.com', category: '产品', picked: true, newsDate: () => new Date() },
{ title: '蚂蚁灵波开源 LingBot-World 2.0,世界模型小时级实时生成', summary: 'LingBot-World 2.0 实现了世界模型的小时级实时生成,是具身智能的重要突破。', source: '量子位', category: '产品', newsDate: () => new Date() },
{ title: '阿里获 ACL 2026 最佳资源论文奖,揭示 Agent 结构性缺陷', summary: '阿里研究团队获得 ACL 2026 最佳资源论文奖,深入分析了当前 Agent 系统的结构性缺陷。', source: '机器之心', category: '论文', newsDate: () => new Date() },
{ title: 'Browser Use CLI 3.0 发布:体积缩小 6 倍,Token 消耗大幅降低', summary: 'Browser Use 工具 CLI 3.0 版本发布,体积缩小 6 倍,Token 消耗显著降低。', source: 'GitHub', category: '工具', newsDate: () => new Date() },
{ title: '腾讯混元 Hy3 正式上线,Agent 任务解决率跃升至 90%', summary: '腾讯混元大模型 Hy3 版本正式上线,在 Agent 任务评测中解决率提升至 90%。', source: '腾讯云', category: '产品', newsDate: () => new Date() },
{ title: 'NVIDIA 发布 B300 GPU:推理性能较 H100 提升 8 倍', summary: 'NVIDIA 发布下一代 Blackwell B300 GPUAI 推理性能较 H100 提升 8 倍,功耗仅增加 20%。', source: 'TechCrunch', category: '产品', hot: true, newsDate: () => new Date() },
{ title: 'OpenAI 被曝开发 GPT-5 代号 Orion,计划年底发布', summary: '据内部消息,OpenAI 正在训练 GPT-5(代号 Orion),预计 2026 年底发布,推理能力将有质的飞跃。', source: 'The Verge', category: '产品', hot: true, newsDate: () => new Date() },
{ title: 'LangChain v0.5 发布:原生支持 MCP 协议与多 Agent 编排', summary: 'LangChain 0.5 版本发布,原生支持 MCP 协议,Agent 编排能力大幅增强。', source: 'GitHub', category: '工具', newsDate: () => new Date() },
{ title: 'Claude Sonnet 4 多项基准超越 GPT-4oAnthropic 估值破 600 亿', summary: 'Anthropic 发布 Claude Sonnet 4,在多项基准测试中超越 GPT-4o,公司估值已突破 600 亿美元。', source: 'TechCrunch', category: '产品', hot: true, newsDate: () => new Date() },
{ title: 'Meta 开源 Llama 4400B 参数 MoE 架构,支持 1M 上下文', summary: 'Meta 正式开源 Llama 4 模型,采用 MoE 架构,400B 参数规模,支持超长上下文。', source: 'Meta AI', category: '产品', hot: true, newsDate: () => new Date() },
{ title: 'GLM-5 发布:智谱 AI 新一代千亿级基座模型', summary: '智谱 AI 发布 GLM-5 基座模型,多项中英文基准超越 Llama 4,全面开放 API。', source: '机器之心', category: '产品', hot: true, newsDate: () => new Date() },
{ title: '清华大学团队提出 Infini-Attention 机制:可处理无限长度上下文', summary: '清华大学研究团队提出 Infini-Attention 机制,理论上可处理无限长度的上下文,大幅降低计算复杂度。', source: 'arXiv', category: '论文', newsDate: () => new Date() },
{ title: 'Runway Gen-4 Alpha 发布:视频生成质量达好莱坞级别', summary: 'Runway 发布 Gen-4 Alpha 模型,视频生成质量和一致性达到好莱坞电影级别。', source: 'Runway', category: '产品', newsDate: () => new Date() },
{ title: 'Cursor 获 2 亿美元 C 轮融资,AI 编程 IDE 估值超 100 亿', summary: 'AI 编程 IDE Cursor 完成 2 亿美元 C 轮融资,估值突破 100 亿美元。', source: 'TechCrunch', category: '公司', newsDate: () => new Date() },
]; ];
async function main() { async function main() {
console.log('📡 AI 趋势数据更新开始...');
const t0 = Date.now();
await mongoose.connect(DB_URI); await mongoose.connect(DB_URI);
console.log('数据库已连接'); const Trend = mongoose.model('Trend', new mongoose.Schema({}, { strict: false, collection: 'trends' }));
// 清除 7 天前的旧数据 let hn = [], arxiv = [];
const weekAgo = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000);
const deleted = await Trend.deleteMany({ newsDate: { $lt: weekAgo } });
console.log(`已清理 ${deleted.deletedCount} 条旧数据`);
// 插入新数据(去重) try { hn = await fetchHN(); console.log(` ✓ HN: ${hn.length}`); }
catch (e) { console.log(` ⚠ HN 失败: ${e.message}`); }
try { arxiv = await fetchArxiv(); console.log(` ✓ arXiv: ${arxiv.length}`); }
catch (e) { console.log(` ⚠ arXiv 失败: ${e.message}`); }
// 合并数据
let items = [...hn, ...arxiv];
// 如果不够,补保底
if (items.length < 6) {
const need = Math.min(FALLBACK.length, MAX_ITEMS - items.length);
const fallback = FALLBACK.slice(0, need).map(f => ({ ...f, newsDate: f.newsDate() }));
items.push(...fallback);
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 } });
// 去重插入
let inserted = 0; let inserted = 0;
for (const item of sampleTrends) { for (const item of items) {
const exists = await Trend.findOne({ title: item.title }); const exists = await Trend.findOne({ title: item.title });
if (!exists) { if (!exists) {
await new Trend({ ...item, status: 'published' }).save(); await new Trend({ ...item, createdAt: new Date(), updatedAt: new Date() }).save();
inserted++; inserted++;
} }
} }
console.log(`新增 ${inserted} 条趋势数据`);
await mongoose.disconnect(); // 总量限制
console.log('完成'); const total = await Trend.countDocuments();
if (total > MAX_ITEMS) {
const over = total - MAX_ITEMS;
const oldest = await Trend.find().sort({ newsDate: 1 }).limit(over);
for (const o of oldest) await Trend.deleteOne({ _id: o._id });
console.log(` → 限 ${MAX_ITEMS} 条,清理 ${over} 条最旧`);
} }
main().catch(e => { console.error('❌', e.message); process.exit(1); }); const finalCount = await Trend.countDocuments();
await mongoose.disconnect();
const elapsed = ((Date.now() - t0) / 1000).toFixed(1);
console.log(`\n✅ 更新完成(${elapsed}s`);
console.log(` HN=${hn.length} arXiv=${arxiv.length} 新增=${inserted} 数据库共=${finalCount}`);
if (items.length > 0) {
console.log('\n📰 最新趋势:');
items.slice(0, Math.min(items.length, 10)).forEach((t, i) => {
const tag = t.hot ? '🔥' : t.picked ? '📌' : ' ';
const cat = t.category?.padEnd(4) || '';
const date = t.newsDate ? new Date(t.newsDate).toLocaleDateString('zh-CN') : '';
console.log(` ${tag} [${cat}] ${t.title} (${t.source})`);
});
}
}
main().catch(e => { console.error('❌ 失败:', e.message); process.exit(1); });