// Fetcher 注册表: 列出所有启用的数据源 // 加新数据源只要: // 1. 写一个 scripts/fetchers/.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 — DB 优先, 已存在的指标名, 透传给 fetcher 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 }) // 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 }