更新首页

This commit is contained in:
cheney
2026-08-06 15:39:59 +08:00
parent ec15f40323
commit 547181d333
23 changed files with 3248 additions and 19 deletions
+122
View File
@@ -0,0 +1,122 @@
// 新闻 fetcher: 每天 3 点调用 mx_finance_search_news 拉国内外经济政治新闻
// 按影响大小排序, 取前 10 条 (LLM 可能多/少几条, 全部入 DB, 按 impactRank 标记)
// 数据直接写 News 表, 与 Indicator 解耦
import axios from 'axios'
import mysql from 'mysql2/promise'
const MCP_BASE_DEFAULT = 'https://mxapi.eastmoney.com/mxds/mcp'
const TIMEOUT = 90000
const TOP_N = 10
const QUERIES = [
'今天国内外最新重大经济政治新闻, 按影响大小排序, 给出前 ' + TOP_N + ' 条, 每条含标题、摘要、发布时间、来源、链接',
]
class McpClient {
constructor(baseUrl, apiKey) { this.baseUrl = baseUrl; this.apiKey = apiKey; this.id = 0 }
async _request(method, params) {
const payload = { jsonrpc: '2.0', id: ++this.id, method, ...(params ? { params } : {}) }
const resp = await axios.post(this.baseUrl, payload, {
timeout: TIMEOUT,
headers: { 'Content-Type': 'application/json', 'Accept': 'application/json, text/event-stream', 'em_api_key': this.apiKey },
responseType: 'text', transformResponse: [(d) => d], validateStatus: () => true,
})
if (resp.status >= 400) throw new Error('HTTP ' + resp.status + ': ' + String(resp.data).slice(0, 300))
const parsed = JSON.parse(String(resp.data || ''))
if (parsed.error) throw new Error('MCP error ' + parsed.error.code + ': ' + parsed.error.message)
return parsed.result || parsed
}
async callTool(name, args = {}) {
const r = await this._request('tools/call', { name, arguments: args })
if (r.isError) throw new Error('tool error: ' + (r.content || []).map((c) => c.text || '').join('; '))
return r
}
}
function parsePublishedAt(s) {
if (!s) return null
const d = new Date(String(s).replace(/-/g, '/'))
return isNaN(d.getTime()) ? null : d
}
function pickNewsSheet(sheets) {
if (!Array.isArray(sheets) || sheets.length === 0) return null
return sheets.find((s) => s.sheetName && (s.sheetName.includes('资讯') || s.sheetName.includes('新闻'))) || sheets[0]
}
// items[i] = [title, summary, publishedAt, source, url]
function parseNewsSheet(sheet, impactBase = 0) {
if (!sheet || !sheet.items) return []
const out = []
for (let i = 0; i < sheet.items.length; i++) {
const r = sheet.items[i]
const title = String(r[0] || '').trim()
if (!title) continue
const publishedAt = parsePublishedAt(r[2])
if (!publishedAt) continue
out.push({
title,
summary: String(r[1] || '').trim().slice(0, 180),
source: String(r[3] || '').trim().slice(0, 80),
url: String(r[4] || '').trim().slice(0, 400),
publishedAt,
impactRank: impactBase + i + 1,
})
}
return out
}
export const META = { name: 'news-mcp', description: '妙想 MCP 新闻资讯 (mx_finance_search_news)' }
export async function fetchNewsList() {
const key = process.env.EM_API_KEY
if (!key) throw new Error('缺少 EM_API_KEY')
const base = process.env.EASTMONEY_MCP_BASE || MCP_BASE_DEFAULT
const client = new McpClient(base, key)
const all = []
let impactBase = 0
for (const q of QUERIES) {
try {
console.log('[News] 调用: ' + q)
const r = await client.callTool('mx_finance_search_news', { query: q })
const text = (r.content || []).map((c) => c.text || '').join('')
const payload = JSON.parse(text)
const sheets = payload.data || (Array.isArray(payload) ? payload : [payload])
const sheet = pickNewsSheet(sheets)
const news = parseNewsSheet(sheet, impactBase)
console.log('[News] 解析 ' + news.length + ' 条')
all.push(...news)
impactBase = all.length
} catch (e) {
console.error('[News] 拉取失败:', e.message)
}
}
return all
}
function parseUrl(url) {
const u = new URL(url)
return { host: u.hostname, port: Number(u.port) || 3306, user: decodeURIComponent(u.username), password: decodeURIComponent(u.password), database: u.pathname.replace(/^\//, '') }
}
export async function persistNews(newsList) {
if (!process.env.DATABASE_URL) throw new Error('缺少 DATABASE_URL')
if (!newsList || newsList.length === 0) return { inserted: 0, skipped: 0 }
const conn = await mysql.createConnection(parseUrl(process.env.DATABASE_URL))
let inserted = 0, skipped = 0
try {
for (const n of newsList) {
const [r] = await conn.execute(
'INSERT INTO `News` (`title`, `summary`, `source`, `url`, `publishedAt`, `impactRank`, `createdAt`) ' +
'VALUES (?, ?, ?, ?, ?, ?, NOW(3)) ' +
'ON DUPLICATE KEY UPDATE `impactRank` = VALUES(`impactRank`)',
[n.title, n.summary, n.source, n.url, n.publishedAt, n.impactRank],
)
if (r.affectedRows === 1) inserted++
else skipped++
}
} finally {
await conn.end()
}
return { inserted, skipped }
}
+9 -1
View File
@@ -147,6 +147,13 @@ async function main() {
console.log('[Refresh] 同步隐藏标记: ' + names.length + ' 个指标 hidden=true (数据保留, 不展示)')
}
// 6) 跨源对比报警: 全量跑一次 (即便无新指标也跑, 防止上次漏检)
try {
const m = await import('../server/alert-engine.js')
const a = await m.checkAfterRefresh(conn)
console.log('[Refresh] 报警检测: 新增 ' + a.inserted + ' 条 (扫描 ' + a.scanned + ')')
} catch (e) { console.error('[Refresh] 报警检测异常:', e.message) }
if (allItems.length === 0) {
console.log('[Refresh] 无指标待写, 不写库 (DB 优先策略生效)')
return
@@ -163,7 +170,7 @@ async function main() {
totalPoints += points
console.log('[Refresh] ' + it.name + (hidden ? ' (hidden)' : '') + ': ' + points + ' 个明细点')
}
console.log('[Refresh] 写入完成: ' + ins + ' 个指标有数据, ' + upd + ' 个无数据, 共 ' + totalPoints + ' 个明细点\n')
} finally {
await conn.end()
@@ -175,3 +182,4 @@ main().catch((e) => {
process.exit(1)
})
+12
View File
@@ -0,0 +1,12 @@
require('dotenv').config()
async function main() {
console.log('[News] 开始拉取...')
const t0 = Date.now()
const mod = await import('./fetchers/news.fetcher.js')
const list = await mod.fetchNewsList()
console.log('[News] 拉取 ' + list.length + ' 条, 耗时 ' + ((Date.now() - t0) / 1000).toFixed(1) + 's')
if (list.length === 0) { console.log('[News] 无数据, 跳过'); return }
const r = await mod.persistNews(list)
console.log('[News] 入库: 新增 ' + r.inserted + ' 条, 重复跳过 ' + r.skipped + ' 条')
}
main().catch((e) => { console.error('[News] 失败:', e); process.exit(1) })