finance-dashboard/lib/yahoo-session.js

72 lines
2.7 KiB
JavaScript
Raw Permalink Normal View History

2026-06-04 09:32:28 +00:00
// 共用 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}&quotesCount=0`;
const j = await yahooJson(url);
await sleep(120);
return j?.news || [];
});
}
export function sleep(ms) {
return new Promise(r => setTimeout(r, ms));
}