// 公司研究:直接連結(SEC/官網 IR)與 10-K 產業鏈合併(不用 Google 搜尋代替資料) import { finalizeIndustryChain, isTradableSymbol, appendDetailNames, SECTOR_SUPPLIER_TICKERS, } from './companyintel-chain.js'; const SEC_UA = 'EmmyInvestDashboard/1.0 (personal learning tool; contact@example.com)'; let _tickerMap = null; async function json(url, headers = {}, ms = 12000) { const ctrl = new AbortController(); const timer = setTimeout(() => ctrl.abort(), ms); try { const res = await fetch(url, { headers: { 'User-Agent': SEC_UA, ...headers }, signal: ctrl.signal }); if (!res.ok) throw new Error(`HTTP ${res.status}`); return await res.json(); } finally { clearTimeout(timer); } } async function tickerToCik(symbol) { if (!_tickerMap) { const d = await json('https://www.sec.gov/files/company_tickers.json', { 'User-Agent': SEC_UA }); _tickerMap = {}; for (const k of Object.keys(d)) { _tickerMap[String(d[k].ticker).toUpperCase()] = { cik: String(d[k].cik_str).padStart(10, '0'), cikNum: Number(d[k].cik_str), name: d[k].title, }; } } return _tickerMap[String(symbol || '').toUpperCase()] || null; } function edgarDocUrl(cikNum, accession, primary) { const accNo = accession.replace(/-/g, ''); return `https://www.sec.gov/Archives/edgar/data/${cikNum}/${accNo}/${primary}`; } /** 投資人關係/官網(不經 Google) */ export function resolveInvestorRelationsUrl(website) { const w = String(website || '').trim(); if (!w) return null; try { const u = new URL(w.startsWith('http') ? w : `https://${w}`); const host = u.hostname.replace(/^www\./, ''); const candidates = [ u.href, `${u.protocol}//${u.host}/investor-relations`, `${u.protocol}//investor.${host}`, `${u.protocol}//ir.${host}`, ]; return { url: candidates[0], labelZh: '公司官網', altUrls: candidates.slice(1) }; } catch { return null; } } /** SEC 直接連結:EDGAR、最新 10-K、DEF 14A */ export async function buildSecResourceLinks(symbol) { const hit = await tickerToCik(symbol); if (!hit) return []; const links = [ { labelZh: 'SEC EDGAR 公司頁', url: `https://www.sec.gov/edgar/browse/?CIK=${hit.cik}`, source: 'SEC' }, ]; try { const sub = await json(`https://data.sec.gov/submissions/CIK${hit.cik}.json`, { 'User-Agent': SEC_UA }); const f = sub.filings?.recent || {}; const found = { '10-K': null, 'DEF 14A': null, '10-Q': null }; for (let i = 0; i < (f.form || []).length; i++) { const form = f.form[i]; if (!found[form] && found[form] !== undefined) { const acc = f.accessionNumber[i]; const doc = f.primaryDocument?.[i]; if (acc && doc) { found[form] = edgarDocUrl(hit.cikNum, acc, doc); } } if (found['10-K'] && found['DEF 14A'] && found['10-Q']) break; } if (found['10-K']) links.push({ labelZh: '最新 10-K 年報', url: found['10-K'], source: 'SEC' }); if (found['10-Q']) links.push({ labelZh: '最新 10-Q 季報', url: found['10-Q'], source: 'SEC' }); if (found['DEF 14A']) links.push({ labelZh: '股東會說明書 DEF 14A', url: found['DEF 14A'], source: 'SEC' }); } catch { /* */ } return links; } function entityGroups(names, label, note) { const list = (names || []).filter(Boolean).slice(0, 10); if (!list.length) return []; return [{ label, entities: list, note }]; } /** 把 10-K 抽出的公司名+產業 fallback 合成上下游結構 */ export function mergeIndustryChainWithHints(symbol, chain, hints = {}, profileExt = {}, profile = {}) { const base = chain || {}; const industry = `${profileExt.industry || ''} ${profileExt.sector || profile.industry || ''}`.toLowerCase(); let upstreamDetail = base.upstreamDetail?.length ? [...base.upstreamDetail] : []; let downstreamDetail = base.downstreamDetail?.length ? [...base.downstreamDetail] : []; let peers = base.peers?.length ? [...base.peers] : [...(profileExt.peers || [])]; if (hints.suppliers?.length) { upstreamDetail = appendDetailNames(upstreamDetail, hints.suppliers, '供應商(10-K)', 'SEC 年報提及', symbol); } if (hints.customers?.length) { downstreamDetail = appendDetailNames(downstreamDetail, hints.customers, '客戶(10-K)', 'SEC 年報提及', symbol); } const ind = industry.toLowerCase(); if (/semiconductor|chip/i.test(ind)) { upstreamDetail = appendDetailNames( upstreamDetail, SECTOR_SUPPLIER_TICKERS.semiconductor, '產業常見供應商', '半導體鏈', symbol, ); } if (hints.competitors?.length) { const from10k = hints.competitors.map(c => String(c).replace(/\s+(Inc\.|Corp\.|Corporation|Ltd\.|LLC|Co\.)/i, '').trim().toUpperCase()) .filter(c => isTradableSymbol(c)); peers = [...new Set([...peers, ...from10k])].filter(p => p !== symbol).slice(0, 12); } const flatUp = upstreamDetail.flatMap(g => g.entities || [g.label]).filter(Boolean); const flatDown = downstreamDetail.flatMap(g => g.entities || [g.label]).filter(Boolean); return finalizeIndustryChain({ ...base, upstream: flatUp.length ? flatUp : base.upstream, downstream: flatDown.length ? flatDown : base.downstream, upstreamDetail, downstreamDetail, peers, tenKExcerpt: hints.excerpt ? String(hints.excerpt).slice(0, 480) : base.tenKExcerpt || null, chainSource: hints.source || base.chainSource || (hints.excerpt ? 'SEC 10-K' : null), chainSources: [...new Set([...(base.chainSources || []), hints.excerpt ? 'SEC 10-K' : null].filter(Boolean))], searches: [], }, symbol); } export async function buildCompanyResources(symbol, profile = {}, management = {}) { const links = []; const ir = resolveInvestorRelationsUrl(profile.website || management.website); if (ir) links.push({ labelZh: '投資人關係/官網', url: ir.url, source: '官網' }); const sec = await buildSecResourceLinks(symbol).catch(() => []); return [...links, ...sec]; }