// Provider registry — pluggable factory + metadata + dynamic helpers // // Adding a built-in provider: // 1. Create providers/.ts implementing Provider + exporting META // 2. Add it to this directory; routing, search, fetch, and config pick it up automatically // // Local-only providers live in ../../pi-search-kit and are loaded when that directory exists. import { existsSync, readdirSync } from "node:fs"; import { dirname, join, resolve } from "node:path"; import { fileURLToPath, pathToFileURL } from "node:url"; import type { Provider, ProviderMeta } from "./types.js"; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); const PROVIDERS_MUTABLE: ProviderMeta[] = []; type ProviderConstructor = new (apiKey: any) => Provider; const PROVIDER_CLASSES = new Map(); async function loadProvidersFromDir(dirPath: string, relativeImportPrefix?: string) { if (!existsSync(dirPath)) return; const files = readdirSync(dirPath); for (const file of files) { if ( (file.endsWith(".ts") || file.endsWith(".js")) && !file.endsWith(".test.ts") && !file.endsWith(".test.js") && !file.endsWith(".spec.ts") && !file.endsWith(".spec.js") && !file.endsWith(".d.ts") && !file.startsWith("index.") && !file.startsWith("types.") ) { try { const nameWithoutExt = file.slice(0, -3); const mod = relativeImportPrefix ? await import(`${relativeImportPrefix}${nameWithoutExt}.js`) : await import(pathToFileURL(join(dirPath, `${nameWithoutExt}.js`)).href); let meta: ProviderMeta | undefined; let providerClass: ProviderConstructor | undefined; for (const [key, value] of Object.entries(mod)) { if (value && typeof value === "object" && "name" in value && "capabilities" in value) { meta = value as ProviderMeta; } if (typeof value === "function" && key.endsWith("Provider")) { providerClass = value as ProviderConstructor; } } if (meta && providerClass) { PROVIDERS_MUTABLE.push(meta); PROVIDER_CLASSES.set(meta.name, providerClass); } } catch (err) { console.error(`Failed to load provider module from ${file}:`, err instanceof Error ? err.message : err); } } } } // 扫描内置服务商目录 await loadProvidersFromDir(__dirname, "./"); // 扫描本地服务商目录 const localDir = resolve(__dirname, "../../pi-search-kit"); await loadProvidersFromDir(localDir); export const PROVIDERS: readonly ProviderMeta[] = PROVIDERS_MUTABLE; export function searchProviderNames(): string[] { return PROVIDERS.filter((p) => p.capabilities.generalSearch).map((p) => p.name); } export function fetchProviderNames(): string[] { return PROVIDERS.filter((p) => p.capabilities.contentExtraction).map((p) => p.name); } export function allVerticals(): string[] { return PROVIDERS.flatMap((p) => p.verticals ?? []); } export function buildSearchChain(): string[] { return PROVIDERS.filter((p) => p.capabilities.generalSearch && p.searchFallbackPriority !== undefined) .sort((a, b) => a.searchFallbackPriority! - b.searchFallbackPriority!) .map((p) => p.name); } export function buildFetchChain(): string[] { return PROVIDERS.filter((p) => p.capabilities.contentExtraction && p.fetchFallbackPriority !== undefined) .sort((a, b) => a.fetchFallbackPriority! - b.fetchFallbackPriority!) .map((p) => p.name); } export function searchPromptGuidelines(): string[] { const lines: string[] = []; // Intro lines.push( "Use web_search for information beyond your training data — current events, recent docs, live data, academic papers, stock prices, CVEs.", ); lines.push("Choose a provider based on the query domain:"); // Per-provider search hints for (const meta of PROVIDERS) { if (meta.searchHint) { lines.push(`- ${meta.label} (provider='${meta.name}'): ${meta.searchHint}`); } } // Vertical enumeration const verts = allVerticals(); const verticalProviders = PROVIDERS.filter((p) => p.capabilities.verticalSearch && p.verticals?.length); if (verticalProviders.length > 0 && verts.length > 0) { lines.push(""); lines.push("For structured vertical data, select the vertical-capable provider AND pass a vertical:"); for (const p of verticalProviders) { lines.push(`- ${p.label}: vertical can be one of ${p.verticals!.map((v) => `'${v}'`).join(", ")}.`); } lines.push("Example: provider='anysearch' + vertical='finance.us_stock' for a stock price query."); } // Closing rules lines.push(""); lines.push( "If unsure which provider fits, omit provider — the extension tries a cost-priority fallback chain (general-purpose first).", ); lines.push('After answering, include a "Sources:" section with markdown hyperlinks: [Title](URL).'); lines.push("Use web_fetch after web_search to read full page content — web_search returns snippets only."); lines.push("Use {queries:[...]} with 2-4 varied angles for broader coverage — each query routes independently."); return lines; } export function fetchPromptGuidelines(): string[] { const lines: string[] = []; // Intro lines.push("Use web_fetch to read the full content of a URL — use after web_search when a snippet is too short."); lines.push("Choose a provider based on the page type:"); // Per-provider fetch hints for (const meta of PROVIDERS) { if (meta.fetchHint) { lines.push(`- ${meta.label} (provider='${meta.name}'): ${meta.fetchHint}`); } } // Closing rules lines.push(""); lines.push( "If unsure which provider fits, omit provider — the extension tries a cost-priority fallback chain (fast/free first, heavy JS-rendering last).", ); lines.push( 'After reading fetched content, include a "Sources:" section with markdown hyperlinks to the fetched URLs.', ); lines.push( "Large pages are truncated — the full content path is reported in the result details, use the read tool to access it.", ); return lines; } export interface ProviderOptions { apiKey: string | undefined; } export function createProvider(name: string, opts: ProviderOptions): Provider { const { apiKey } = opts; const meta = PROVIDERS.find((p) => p.name === name); if (!meta) { throw new Error(`Unknown provider: "${name}". Available: ${PROVIDERS.map((p) => p.name).join(", ")}`); } const keyRequired = meta.apiKeyRequired ?? true; if (keyRequired && !apiKey) { throw new Error(`${meta.label} requires an API key`); } const ProviderClass = PROVIDER_CLASSES.get(name); if (!ProviderClass) { throw new Error(`Implementation class for provider "${name}" not found`); } return new ProviderClass(apiKey); } export function createAvailableProviders(apiKeys: Record): Map { const providers = new Map(); for (const meta of PROVIDERS) { const key = apiKeys[meta.name]; const keyRequired = meta.apiKeyRequired ?? true; if (!key && keyRequired) continue; try { providers.set(meta.name, createProvider(meta.name, { apiKey: key })); } catch (err) { console.error(`Failed to initialize provider "${meta.name}":`, err instanceof Error ? err.message : err); // skip providers that fail to initialize } } return providers; }