84 lines
3.4 KiB
JavaScript
84 lines
3.4 KiB
JavaScript
// Fetcher 注册表: 列出所有启用的数据源
|
|
// 加新数据源只要:
|
|
// 1. 写一个 scripts/fetchers/<source>.fetcher.js, 导出 META + fetchAll
|
|
// 2. 在本文件 FETCHERS 数组里加一行
|
|
// 3. 完事, refresh 脚本会自动跑
|
|
|
|
// 每项的 schema:
|
|
// name: 数据源标识 (用于日志)
|
|
// module: fetcher 模块路径 (相对 scripts/fetchers/)
|
|
// enabled: true/false, 可由 env 关闭 (如 ENABLE_EASTMONEY_MCP=0)
|
|
// env: 需要的环境变量名列表, 缺一即跳过
|
|
export const FETCHERS = [
|
|
{
|
|
name: 'wb-indicators',
|
|
module: './wb-indicators.fetcher.js',
|
|
enabled: true,
|
|
env: [],
|
|
},
|
|
{
|
|
name: 'eastmoney-mcp',
|
|
module: './eastmoney-mcp.fetcher.js',
|
|
// 默认开启, 有 APIKey 才真正能跑 (fetcher 内部会自检)
|
|
enabled: process.env.ENABLE_EASTMONEY_MCP !== '0',
|
|
env: ['EM_API_KEY'],
|
|
},
|
|
// 后续在这里追加:
|
|
// { name: 'fred', module: './fred.fetcher.js', enabled: true, env: ['FRED_API_KEY'] },
|
|
// { name: 'tushare', module: './tushare.fetcher.js', enabled: true, env: ['TUSHARE_TOKEN'] },
|
|
]
|
|
|
|
// 校验环境变量, 返回 { ok, missing[] }
|
|
export function checkEnv(required) {
|
|
const missing = required.filter((k) => !process.env[k] || !process.env[k].trim())
|
|
return { ok: missing.length === 0, missing }
|
|
}
|
|
|
|
// 加载并运行所有启用的 fetcher, 返回 [{ name, ok, items, error, skipped }]
|
|
// 错误隔离: 一个 fetcher 挂了不影响其他
|
|
// opts.skipNames: Set<string> — DB 优先, 已存在的指标名, 透传给 fetcher
|
|
// opts.forceNames: Set<string> — 强制刷新名单, 即使 skipNames 里有也照拉
|
|
export async function runFetchers(opts = {}) {
|
|
const results = []
|
|
for (const meta of FETCHERS) {
|
|
if (!meta.enabled) {
|
|
results.push({ name: meta.name, skipped: true, reason: 'disabled' })
|
|
console.log(`[Fetcher] ${meta.name} 跳过 (disabled)`)
|
|
continue
|
|
}
|
|
const envCheck = checkEnv(meta.env)
|
|
if (!envCheck.ok) {
|
|
results.push({ name: meta.name, skipped: true, reason: `缺少 env: ${envCheck.missing.join(', ')}` })
|
|
console.log(`[Fetcher] ${meta.name} 跳过 (${envCheck.missing.join(', ')})`)
|
|
continue
|
|
}
|
|
try {
|
|
const mod = await import(meta.module)
|
|
if (!mod.META || !mod.fetchAll) {
|
|
throw new Error('fetcher 必须导出 META 和 fetchAll')
|
|
}
|
|
console.log(`[Fetcher] ${meta.name} 开始...`)
|
|
const out = await mod.fetchAll({ skipNames: opts.skipNames, onlyNames: opts.onlyNames, forceNames: opts.forceNames })
|
|
// fetcher 可返回 { skipped: true, reason } 表示"全部被 DB 优先跳过", 不算失败
|
|
if (out && out.skipped) {
|
|
results.push({ name: meta.name, skipped: true, reason: out.reason })
|
|
console.log(`[Fetcher] ${meta.name} 跳过 (${out.reason})`)
|
|
continue
|
|
}
|
|
const items = Array.isArray(out) ? out : []
|
|
// 校验返回结构
|
|
const valid = items.filter((it) => it && it.name && it.data && Array.isArray(it.data.rows))
|
|
if (valid.length === 0) {
|
|
throw new Error('fetcher 未返回任何有效指标 (需包含 name/data.rows)')
|
|
}
|
|
console.log(`[Fetcher] ${meta.name} 拉取 ${valid.length} 个指标`)
|
|
results.push({ name: meta.name, ok: true, items: valid })
|
|
} catch (e) {
|
|
results.push({ name: meta.name, ok: false, error: e.message })
|
|
console.error(`[Fetcher] ${meta.name} 失败: ${e.message}`)
|
|
}
|
|
}
|
|
return results
|
|
}
|
|
|