// 共用 Yahoo Finance cookie/crumb(避免多模組並行請求互相打掛) const UA = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124 Safari/537.36'; let _auth = { cookie: null, crumb: null, at: 0 }; let _inflight = null; let _queue = Promise.resolve(); export function resetYahooAuth() { _auth = { cookie: null, crumb: null, at: 0 }; } export async function yahooAuth(force = false) { if (!force && _auth.crumb && Date.now() - _auth.at < 3600e3) return _auth; if (_inflight) return _inflight; _inflight = (async () => { const r1 = await fetch('https://fc.yahoo.com/', { headers: { 'User-Agent': UA } }).catch(() => null); const cookie = (r1?.headers.get('set-cookie') || '').split(';')[0] || ''; const r2 = await fetch('https://query2.finance.yahoo.com/v1/test/getcrumb', { headers: { 'User-Agent': UA, Cookie: cookie }, }); const crumb = (await r2.text()).trim(); if (!crumb || crumb.includes('<')) throw new Error('Yahoo crumb'); _auth = { cookie, crumb, at: Date.now() }; return _auth; })().finally(() => { _inflight = null; }); return _inflight; } async function yahooJson(url, retry = true) { const { cookie, crumb } = await yahooAuth(); const sep = url.includes('?') ? '&' : '?'; const full = `${url}${sep}crumb=${encodeURIComponent(crumb)}`; const res = await fetch(full, { headers: { 'User-Agent': UA, Cookie: cookie } }); if ((res.status === 401 || res.status === 429) && retry) { resetYahooAuth(); await sleep(500); await yahooAuth(true); return yahooJson(url, false); } if (!res.ok) throw new Error(`Yahoo HTTP ${res.status}`); return res.json(); } function yahooQueued(fn) { const run = _queue.then(() => fn()); _queue = run.catch(() => {}); return run; } /** quoteSummary 模組(assetProfile、topHoldings 等)— 序列化避免並行打掛 crumb */ export async function yahooQuoteSummary(symbol, modules) { return yahooQueued(async () => { const mod = Array.isArray(modules) ? modules.join(',') : modules; const url = `https://query2.finance.yahoo.com/v10/finance/quoteSummary/${encodeURIComponent(symbol)}?modules=${encodeURIComponent(mod)}`; const j = await yahooJson(url); await sleep(120); return j?.quoteSummary?.result?.[0] || null; }); } export async function yahooFinanceSearchNews(symbol, count = 12) { return yahooQueued(async () => { const url = `https://query1.finance.yahoo.com/v1/finance/search?q=${encodeURIComponent(symbol)}&newsCount=${count}"esCount=0`; const j = await yahooJson(url); await sleep(120); return j?.news || []; }); } export function sleep(ms) { return new Promise(r => setTimeout(r, ms)); }