UNPKG

autosnippet

Version:

Extract code patterns into a knowledge base for AI coding assistants

241 lines (240 loc) 9.53 kB
/** * AiFactory - AI 提供商工厂 * * 根据配置/环境变量创建对应的 AI Provider 实例 * 支持: google-gemini, openai, deepseek, claude, ollama, mock */ import Logger from '../../infrastructure/logging/Logger.js'; import { ClaudeProvider } from './providers/ClaudeProvider.js'; import { GoogleGeminiProvider } from './providers/GoogleGeminiProvider.js'; import { MockProvider } from './providers/MockProvider.js'; import { OpenAiProvider } from './providers/OpenAiProvider.js'; const PROVIDER_MAP = { google: GoogleGeminiProvider, 'google-gemini': GoogleGeminiProvider, gemini: GoogleGeminiProvider, openai: OpenAiProvider, deepseek: OpenAiProvider, claude: ClaudeProvider, anthropic: ClaudeProvider, ollama: OpenAiProvider, mock: MockProvider, }; const DEEPSEEK_BASE = 'https://api.deepseek.com/v1'; /** * 创建 AI Provider 实例 * @param options {provider, model, apiKey, baseUrl} */ export function createProvider(options = {}) { const provider = options.provider || process.env.ASD_AI_PROVIDER || 'google'; const ProviderClass = PROVIDER_MAP[provider.toLowerCase()]; if (!ProviderClass) { throw new Error(`Unknown AI provider: ${provider}. Supported: ${Object.keys(PROVIDER_MAP).join(', ')}`); } const config = { ...options }; // 针对不同 provider 设置默认值 switch (provider.toLowerCase()) { case 'deepseek': config.name = 'deepseek'; config.baseUrl = config.baseUrl || DEEPSEEK_BASE; config.apiKey = config.apiKey || process.env.ASD_DEEPSEEK_API_KEY || ''; config.model = config.model || 'deepseek-chat'; break; case 'ollama': config.name = 'ollama'; config.baseUrl = config.baseUrl || process.env.ASD_OLLAMA_BASE_URL || 'http://localhost:11434/v1'; config.apiKey = config.apiKey || 'ollama'; config.model = config.model || 'llama3'; config.embedModel = config.embedModel || 'qwen3-embedding:0.6b'; break; default: break; } return new ProviderClass(config); } /** * 从环境变量自动探测并创建 Provider * 优先级: ASD_AI_PROVIDER 指定 > 有 key 的第一个 */ export function autoDetectProvider() { const logger = Logger.getInstance(); const explicit = process.env.ASD_AI_PROVIDER; if (explicit && explicit.toLowerCase() !== 'auto') { // 验证显式指定的 provider 是否有对应 API Key const keyEnvMap = { google: 'ASD_GOOGLE_API_KEY', 'google-gemini': 'ASD_GOOGLE_API_KEY', gemini: 'ASD_GOOGLE_API_KEY', openai: 'ASD_OPENAI_API_KEY', deepseek: 'ASD_DEEPSEEK_API_KEY', claude: 'ASD_CLAUDE_API_KEY', anthropic: 'ASD_CLAUDE_API_KEY', ollama: null, // Ollama 不需要 key mock: null, }; const requiredKeyEnv = keyEnvMap[explicit.toLowerCase()]; if (requiredKeyEnv && !process.env[requiredKeyEnv]) { logger.warn(`[AiFactory] ASD_AI_PROVIDER=${explicit} 但 ${requiredKeyEnv} 未配置,尝试自动探测其他可用 provider…`); // 降级到自动探测,不直接 return } else { logger.debug(`AI provider explicitly set: ${explicit}`); return createProvider({ provider: explicit }); } } // 按优先级探测 if (process.env.ASD_GOOGLE_API_KEY) { logger.debug('Auto-detected Google Gemini provider'); return createProvider({ provider: 'google' }); } if (process.env.ASD_OPENAI_API_KEY) { logger.debug('Auto-detected OpenAI provider'); return createProvider({ provider: 'openai' }); } if (process.env.ASD_CLAUDE_API_KEY) { logger.debug('Auto-detected Claude provider'); return createProvider({ provider: 'claude' }); } if (process.env.ASD_DEEPSEEK_API_KEY) { logger.debug('Auto-detected DeepSeek provider'); return createProvider({ provider: 'deepseek' }); } logger.info('[AiFactory] 未找到任何 AI API Key,AI 功能已跳过。请在 .env 中配置 ASD_GOOGLE_API_KEY 等。'); return createProvider({ provider: 'mock' }); } // ─── Fallback 机制 ────────────────────────────────────────── const PROVIDER_KEY_MAP = { google: 'ASD_GOOGLE_API_KEY', openai: 'ASD_OPENAI_API_KEY', deepseek: 'ASD_DEEPSEEK_API_KEY', claude: 'ASD_CLAUDE_API_KEY', }; /** 获取可用的 fallback provider 列表(排除当前 provider) */ export function getAvailableFallbacks(currentProvider) { const fallbacks = []; for (const [name, envKey] of Object.entries(PROVIDER_KEY_MAP)) { if (name === currentProvider) { continue; } const key = process.env[envKey]; if (key && key.length > 0) { fallbacks.push(name); } } return fallbacks; } /** 判断是否为地理限制 / 不可恢复的 provider 级错误(应触发 fallback) */ export function isGeoOrProviderError(err) { const msg = (err.message || '').toLowerCase(); return (/user location is not supported|failed_precondition|unsupported.*(region|country|location)|geo|blocked/i.test(msg) || (/permission.*denied|forbidden/i.test(msg) && !/rate.?limit|quota|429/i.test(msg))); } /** * 获取 AI Provider,带自动 fallback: * 当主 provider 调用失败(地理限制等)时自动切换到备选 provider */ export async function getProviderWithFallback() { const logger = Logger.getInstance(); const primary = autoDetectProvider(); if (!primary) { return null; } const currentProvider = (process.env.ASD_AI_PROVIDER || 'google').toLowerCase(); // 用 probe 测试 primary 是否可用 try { if (typeof primary.probe === 'function') { await primary.probe(); } return primary; } catch (probeErr) { if (!isGeoOrProviderError(probeErr)) { // 非地理限制,可能是临时网络问题,仍返回 primary return primary; } logger.warn(`[AiFactory] Primary provider "${currentProvider}" failed: ${probeErr.message}`); } // Primary 确认不可用,尝试 fallback const fallbacks = getAvailableFallbacks(currentProvider); if (fallbacks.length === 0) { logger.warn(`[AiFactory] No fallback providers available. Primary: ${currentProvider}`); return primary; } for (const fbName of fallbacks) { try { logger.info(`[AiFactory] Trying fallback provider: ${fbName}`); const fbProvider = createProvider({ provider: fbName }); fbProvider._fallbackFrom = currentProvider; return fbProvider; } catch (e) { logger.warn(`[AiFactory] Fallback "${fbName}" creation failed: ${e.message}`); } } return primary; } /** * 创建独立的 Embedding Provider * * 当 ASD_EMBED_PROVIDER 被设置时,创建一个专用于 embedding 的 provider 实例, * 使 embedding 和 LLM 生成可以使用不同的提供商/模型。 * * 典型场景:LLM 用 Google Gemini,Embedding 用本地 Ollama + qwen3-embedding * * @returns 独立的 embed provider,或 null(未配置时) */ export function createEmbedProvider() { const embedProviderName = process.env.ASD_EMBED_PROVIDER; if (!embedProviderName) { return null; } const logger = Logger.getInstance(); logger.info(`[AiFactory] Creating dedicated embed provider: ${embedProviderName}`); return createProvider({ provider: embedProviderName, model: process.env.ASD_EMBED_MODEL || undefined, baseUrl: process.env.ASD_EMBED_BASE_URL || undefined, apiKey: process.env.ASD_EMBED_API_KEY || undefined, embedModel: process.env.ASD_EMBED_MODEL || undefined, }); } /** 获取当前 AI 配置信息(同步,用于 UI 展示) */ export function getAiConfigInfo() { const provider = process.env.ASD_AI_PROVIDER || 'auto'; const model = process.env.ASD_AI_MODEL || ''; const embedProvider = process.env.ASD_EMBED_PROVIDER || ''; const embedModel = process.env.ASD_EMBED_MODEL || ''; const hasGoogleKey = !!process.env.ASD_GOOGLE_API_KEY; const hasOpenAiKey = !!process.env.ASD_OPENAI_API_KEY; const hasClaudeKey = !!process.env.ASD_CLAUDE_API_KEY; const hasDeepSeekKey = !!process.env.ASD_DEEPSEEK_API_KEY; return { provider, model, embedProvider, embedModel, hasKey: hasGoogleKey || hasOpenAiKey || hasClaudeKey || hasDeepSeekKey, keys: { google: hasGoogleKey, openai: hasOpenAiKey, claude: hasClaudeKey, deepseek: hasDeepSeekKey, }, }; } // 所有提供商的集中导出 export { AiProvider } from './AiProvider.js'; export { ClaudeProvider } from './providers/ClaudeProvider.js'; export { GoogleGeminiProvider } from './providers/GoogleGeminiProvider.js'; export { MockProvider } from './providers/MockProvider.js'; export { OpenAiProvider } from './providers/OpenAiProvider.js'; export default { createProvider, createEmbedProvider, autoDetectProvider, getAiConfigInfo, getProviderWithFallback, getAvailableFallbacks, isGeoOrProviderError, };