更新首页
This commit is contained in:
@@ -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 }
|
||||
}
|
||||
@@ -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)
|
||||
})
|
||||
|
||||
|
||||
|
||||
@@ -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) })
|
||||
Reference in New Issue
Block a user