import { Injectable, Logger } from '@nestjs/common'; interface ChatMessage { role: 'user' | 'assistant' | 'system'; content: string | { type: 'text' | 'image_url'; text?: string; image_url?: { url: string } }[]; } export interface ChatOptions { temperature?: number; top_p?: number; max_tokens?: number; } export interface ModelInfo { id: string; provider: string; capabilities: string[]; contextWindow: number; } interface AIProvider { name: string; chat(messages: ChatMessage[], options?: ChatOptions): Promise; chatStream?(messages: ChatMessage[], options?: ChatOptions): AsyncIterable; } // 仅收录沙箱实际可用的国内已备案大模型 const MODEL_CATALOG: Record = { 'deepseek-v4-flash': { id: 'deepseek-v4-flash', provider: '硅基流动/DeepSeek', capabilities: ['chat', 'code'], contextWindow: 32768 }, 'sensenova-6.7-flash-lite': { id: 'sensenova-6.7-flash-lite', provider: '商汤科技', capabilities: ['chat', 'code'], contextWindow: 32768 }, }; /** 沙箱可选模型列表(前端展示用),modelId 必须与 MODEL_CATALOG 中的 id 一致 */ export const SANDBOX_MODELS = [ { id: 'deepseek-v4-flash', label: 'DeepSeek V4 Flash', provider: '硅基流动', desc: '高速推理,代码生成能力强,价格极低', available: false }, { id: 'sensenova-6.7-flash-lite', label: 'SenseNova 6.7 Flash Lite', provider: '商汤科技', desc: '轻量快速,日常对话与文本处理', available: false }, ]; @Injectable() export class AIGatewayService { private readonly logger = new Logger(AIGatewayService.name); private providers: Map = new Map(); private usageStats: Record = {}; constructor() { this.registerProviders(); } private registerProviders() { // 国内已备案大模型 — 通过硅基流动 / DeepSeek 直连等国内渠道 if (process.env.OPENAI_API_KEY) { let apiUrl = process.env.OPENAI_API_URL || 'https://api.openai.com/v1/chat/completions'; if (!apiUrl.endsWith('/chat/completions')) { apiUrl = apiUrl.replace(/\/+$/, '') + '/chat/completions'; } const defaultModel = process.env.OPENAI_MODEL || 'deepseek/deepseek-v4-flash'; this.providers.set('deepseek-v4-flash', new OpenAICompatibleProvider( process.env.OPENAI_API_KEY!, apiUrl, defaultModel, '硅基流动 / DeepSeek', )); this.logger.log(`DeepSeek(通过国内渠道)已注册: ${defaultModel}`); this.markAvailable('deepseek-v4-flash'); } if (process.env.SENSENOVA_API_KEY) { let apiUrl = process.env.SENSENOVA_API_URL || 'https://token.sensenova.cn/v1'; if (!apiUrl.endsWith('/chat/completions')) { apiUrl = apiUrl.replace(/\/+$/, '') + '/chat/completions'; } const models = ['deepseek-v4-flash', 'sensenova-6.7-flash-lite']; for (const modelName of models) { this.providers.set(modelName, new OpenAICompatibleProvider( process.env.SENSENOVA_API_KEY!, apiUrl, modelName, '商汤科技', )); this.markAvailable(modelName); } this.logger.log(`商汤科技已注册: ${models.join(', ')}`); } } /** 标记 SANDBOX_MODELS 中的对应模型为可用 */ private markAvailable(modelId: string) { for (const m of SANDBOX_MODELS) { if (m.id === modelId) m.available = true; } } async chat(model: string, messages: ChatMessage[], options?: ChatOptions): Promise { // 仅路由到国内已备案模型的 provider const modelMap: Record = { 'deepseek-v4-flash': 'deepseek-v4-flash', 'sensenova-6.7-flash-lite': 'sensenova-6.7-flash-lite', }; const providerKey = modelMap[model.toLowerCase()] || model; const provider = this.providers.get(providerKey); if (!this.usageStats[providerKey]) this.usageStats[providerKey] = { total: 0, success: 0, failed: 0 }; this.usageStats[providerKey].total++; if (provider) { try { const reply = await provider.chat(messages, options); if (typeof reply !== 'string' || reply.length === 0) { throw new Error(`AI 返回内容为空: ${JSON.stringify(reply)}`); } this.usageStats[providerKey].success++; return reply; } catch (err: any) { this.logger.error(`${provider.name} 调用失败: ${err.message}`); this.usageStats[providerKey].failed++; return this.fallback(messages); } } return this.fallback(messages); } async *chatStream(model: string, messages: ChatMessage[], options?: ChatOptions): AsyncIterable { const modelMap: Record = { 'deepseek-v4-flash': 'deepseek-v4-flash', 'sensenova-6.7-flash-lite': 'sensenova-6.7-flash-lite', }; const providerKey = modelMap[model.toLowerCase()] || model; const provider = this.providers.get(providerKey); if (!this.usageStats[providerKey]) this.usageStats[providerKey] = { total: 0, success: 0, failed: 0 }; this.usageStats[providerKey].total++; if (provider?.chatStream) { try { let hasContent = false; for await (const chunk of provider.chatStream(messages, options)) { hasContent = true; yield chunk; } if (!hasContent) throw new Error('AI 返回内容为空'); this.usageStats[providerKey].success++; } catch (err: any) { this.logger.error(`${provider.name} 流式调用失败: ${err.message}`); this.usageStats[providerKey].failed++; yield this.fallback(messages); } } else if (provider) { try { const reply = await provider.chat(messages, options); this.usageStats[providerKey].success++; yield reply; } catch (err: any) { this.logger.error(`${provider.name} 调用失败: ${err.message}`); this.usageStats[providerKey].failed++; yield this.fallback(messages); } } else { yield this.fallback(messages); } } private fallback(messages: ChatMessage[]): string { const lastMsg = typeof messages[messages.length - 1]?.content === 'string' ? messages[messages.length - 1]?.content as string : ''; const mockReplies: Record = { '你好': '你好!我是宇之然 AI 助手,很高兴为你服务!', 'hello': 'Hello! I am YuZhiRan AI assistant, nice to meet you!', }; for (const [key, reply] of Object.entries(mockReplies)) { if (lastMsg.toLowerCase().includes(key)) { return reply; } } if (lastMsg.includes('提示词') || lastMsg.includes('prompt')) { return '好的提示词需要明确角色、任务、输出格式和约束条件。例如:"你是一名专业的文案编辑,请帮我优化以下产品描述,要求语言简洁有力,突出产品核心卖点,控制在200字以内。"'; } if (lastMsg.includes('模型') || lastMsg.includes('大模型')) { return '目前主流的 AI 大模型包括:OpenAI 的 GPT 系列、Anthropic 的 Claude 系列、Google 的 Gemini 系列,以及国内的 DeepSeek、通义千问、文心一言、GLM 等。各模型在语言理解、代码生成、逻辑推理等方面各有优势。'; } const names = Array.from(this.providers.values()).map(p => p.name).join('、'); return `我是宇之然 AI 助手。关于"${lastMsg.slice(0, 50)}..."的问题,我已收到。当前 AI 沙箱处于模拟模式,请配置 API Key 以获取真实回复。已配置的 API:${names}。`; } getModelInfo(model: string): ModelInfo { const key = model.toLowerCase(); return MODEL_CATALOG[key] || { id: model, provider: '未知', capabilities: ['chat'], contextWindow: 4096 }; } getUsageStats() { return { ...this.usageStats }; } getRegisteredProviders(): string[] { return Array.from(this.providers.keys()); } /** 返回沙箱前端可选的模型列表(仅返回有 provider 注册的模型) */ getSandboxModels() { return SANDBOX_MODELS.filter(m => m.available); } } class OpenAICompatibleProvider implements AIProvider { name: string; private apiKey: string; private apiUrl: string; private defaultModel: string; constructor(apiKey: string, apiUrl: string, defaultModel: string, name?: string) { this.apiKey = apiKey; this.apiUrl = apiUrl; this.defaultModel = defaultModel; this.name = name || 'OpenAI 兼容接口'; } async chat(messages: ChatMessage[], options?: ChatOptions): Promise { const res = await fetch(this.apiUrl, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${this.apiKey}`, }, body: JSON.stringify({ model: this.defaultModel, messages, temperature: options?.temperature ?? 0.7, top_p: options?.top_p ?? 1, max_tokens: options?.max_tokens ?? 2000, }), }); if (!res.ok) { throw new Error(`${this.name} API error: ${res.status} ${await res.text()}`); } const data = await res.json() as any; const msg = data.choices[0].message; return msg.content || msg.reasoning_content || ''; } async *chatStream(messages: ChatMessage[], options?: ChatOptions): AsyncIterable { const res = await fetch(this.apiUrl, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${this.apiKey}`, }, body: JSON.stringify({ model: this.defaultModel, messages, temperature: options?.temperature ?? 0.7, top_p: options?.top_p ?? 1, max_tokens: options?.max_tokens ?? 2000, stream: true, }), }); if (!res.ok) { throw new Error(`${this.name} API error: ${res.status} ${await res.text()}`); } const reader = res.body!.getReader(); const decoder = new TextDecoder(); let buffer = ''; while (true) { const { done, value } = await reader.read(); if (done) break; buffer += decoder.decode(value, { stream: true }); const lines = buffer.split('\n'); buffer = lines.pop() || ''; for (const line of lines) { const trimmed = line.trim(); if (!trimmed || !trimmed.startsWith('data:')) continue; const data = trimmed.slice(5).trim(); if (data === '[DONE]') return; try { const json = JSON.parse(data); const delta = json.choices?.[0]?.delta || {}; const content = delta.content || delta.reasoning_content || ''; if (content) yield content; } catch {} } } } }