import { createMarketController, type CreateMarketControllerOptions } from "./market/index"; import { renderMarketAuthGate } from "./market/auth-gate"; import { isAuthResponseMessage, type AuthStateValue } from "../shared/auth-messages"; interface ChromeRuntimeLike { getURL?: (path: string) => string; id?: string; sendMessage?: (message: unknown) => void | Promise; } const DOWNLOAD_MARKET_CSV_MESSAGE = "download-market-csv"; interface BootContentScriptOptions { createMarketController?: ( options: CreateMarketControllerOptions ) => { dispose?: () => void; ready: Promise }; document?: Document; sendAuthMessage?: (message: unknown) => Promise; window?: Window; } export async function bootContentScript( options: BootContentScriptOptions = {} ): Promise<{ ready: Promise } | null> { const currentWindow = options.window ?? window; const currentDocument = options.document ?? document; const controllerFactory = options.createMarketController ?? createMarketController; const sendAuthMessage = options.sendAuthMessage ?? createRuntimeMessageSender(); if (!isMarketPage(currentWindow.location.href)) { return null; } installMarketPageBridge(currentDocument); const authState = await readAuthState(sendAuthMessage); if (!authState?.isAuthenticated) { await waitForBodyReady(currentDocument, currentWindow); renderMarketAuthGate( currentDocument, currentWindow, isExpiredAuthState(authState) ? "登录已过期,请重新登录" : undefined ); return { ready: Promise.resolve() }; } await waitForBodyReady(currentDocument, currentWindow); return controllerFactory({ document: currentDocument, onCsvReady: (csv: string, filename?: string) => { if (filename) { downloadCsv(currentDocument, currentWindow, csv, filename); return; } if (requestCsvDownload(csv)) { return; } downloadCsv(currentDocument, currentWindow, csv, filename); }, window: currentWindow }); } async function readAuthState( sendMessage: (message: unknown) => Promise ): Promise { const response = await sendMessage({ type: "auth:get-state" }); if (!isAuthResponseMessage(response) || !response.ok || response.type !== "auth:state") { return null; } return response.value; } function isMarketPage(url: string): boolean { const parsedUrl = new URL(url); const isXingtuHost = parsedUrl.hostname === "xingtu.cn" || parsedUrl.hostname.endsWith(".xingtu.cn"); return isXingtuHost && parsedUrl.pathname.startsWith("/ad/creator/market"); } function bootstrapContentScript() { const runtime = ( globalThis as typeof globalThis & { chrome?: { runtime?: ChromeRuntimeLike }; } ).chrome?.runtime; if (!runtime || typeof window === "undefined" || typeof document === "undefined") { return; } const marker = "__starChartSearchEnhancerContentController"; const scopedWindow = window as Window & { [marker]?: boolean | { dispose?: () => void; ready: Promise } | null; }; if (scopedWindow[marker]) { return; } scopedWindow[marker] = true; void bootContentScript().then((controller) => { scopedWindow[marker] = controller; }); } bootstrapContentScript(); function requestCsvDownload(csv: string, filename?: string): boolean { const runtime = ( globalThis as typeof globalThis & { chrome?: { runtime?: ChromeRuntimeLike }; } ).chrome?.runtime; if (!runtime?.id || typeof runtime.sendMessage !== "function") { return false; } runtime.sendMessage({ csv, filename: filename ?? `star-chart-search-enhancer-${formatTimestampForFilename()}.csv`, type: DOWNLOAD_MARKET_CSV_MESSAGE }); return true; } function createRuntimeMessageSender(): (message: unknown) => Promise { return async (message: unknown) => { const runtime = ( globalThis as typeof globalThis & { chrome?: { runtime?: ChromeRuntimeLike }; } ).chrome?.runtime; if (typeof runtime?.sendMessage !== "function") { return null; } return runtime.sendMessage(message); }; } async function waitForBodyReady(document: Document, currentWindow: Window): Promise { if (document.body) { return; } await new Promise((resolve) => { const handleReady = () => { if (document.body) { document.removeEventListener("DOMContentLoaded", handleReady); resolve(); } }; document.addEventListener("DOMContentLoaded", handleReady); currentWindow.setTimeout(handleReady, 0); }); } function downloadCsv( document: Document, window: Window, csv: string, filename?: string ): void { const blob = new Blob(["\uFEFF", csv], { type: "text/csv;charset=utf-8" }); const objectUrl = window.URL.createObjectURL(blob); const link = document.createElement("a"); link.href = objectUrl; link.download = filename ?? `star-chart-search-enhancer-${formatTimestampForFilename()}.csv`; document.body.appendChild(link); link.click(); link.remove(); window.URL.revokeObjectURL(objectUrl); } function formatTimestampForFilename(): string { return new Date().toISOString().replace(/[:.]/g, "-"); } function isExpiredAuthState(authState: AuthStateValue | null): boolean { const lastError = authState?.lastError; return ( typeof lastError === "string" && (/token/i.test(lastError) || lastError.includes("过期")) ); } function installMarketPageBridge(document: Document) { if ( document.documentElement.querySelector( '[data-sces-market-bridge="script"]' ) ) { return; } const script = document.createElement("script"); script.dataset.scesMarketBridge = "script"; const runtime = ( globalThis as typeof globalThis & { chrome?: { runtime?: ChromeRuntimeLike }; } ).chrome?.runtime; const bridgeUrl = runtime?.getURL?.("content/market-page-bridge.js"); if (bridgeUrl) { script.src = bridgeUrl; } else { script.textContent = ""; } (document.head ?? document.documentElement).appendChild(script); }