feat: Phase 1-3 全部完成 — 沙盒增强、学情分析、学习路径

This commit is contained in:
yuzhiran-dev
2026-05-18 09:48:51 +08:00
commit 11bb86854c
277 changed files with 37755 additions and 0 deletions
@@ -0,0 +1,158 @@
import { Injectable, Logger } from '@nestjs/common';
interface ChatMessage {
role: 'user' | 'assistant' | 'system';
content: string;
}
export interface ChatOptions {
temperature?: number;
top_p?: number;
max_tokens?: number;
}
interface AIProvider {
name: string;
chat(messages: ChatMessage[], options?: ChatOptions): Promise<string>;
}
@Injectable()
export class AIGatewayService {
private readonly logger = new Logger(AIGatewayService.name);
private providers: Map<string, AIProvider> = new Map();
constructor() {
this.registerProviders();
}
private registerProviders() {
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 || 'gpt-3.5-turbo';
this.providers.set('openai', new OpenAICompatibleProvider(
process.env.OPENAI_API_KEY!,
apiUrl,
defaultModel,
'OpenAI 兼容接口',
));
this.logger.log(`OpenAI 兼容接口已注册: ${defaultModel}`);
}
if (process.env.OPENCODE_API_KEY) {
let apiUrl = process.env.OPENCODE_API_URL || 'https://opencode.ai/zen/go/v1';
if (!apiUrl.endsWith('/chat/completions')) {
apiUrl = apiUrl.replace(/\/+$/, '') + '/chat/completions';
}
const defaultModel = process.env.OPENCODE_MODEL || 'deepseek-v4-flash';
this.providers.set('opencode', new OpenAICompatibleProvider(
process.env.OPENCODE_API_KEY!,
apiUrl,
defaultModel,
'OpenCode Go',
));
this.logger.log(`OpenCode Go 已注册: ${defaultModel}`);
}
}
async chat(model: string, messages: ChatMessage[], options?: ChatOptions): Promise<string> {
const modelMap: Record<string, string> = {
'general': 'openai',
'openai': 'openai',
'gpt-3.5': 'openai',
'gpt-4': 'openai',
'longcat': 'openai',
'meituan/longcat-flash-lite': 'openai',
'opencode-go': 'opencode',
'opencode': 'opencode',
'deepseek-v4-flash': 'opencode',
};
const providerKey = modelMap[model.toLowerCase()] || (model.includes('/') ? 'openai' : model);
const provider = this.providers.get(providerKey);
if (provider) {
try {
const reply = await provider.chat(messages, options);
if (typeof reply !== 'string' || reply.length === 0) {
throw new Error(`AI 返回内容为空: ${JSON.stringify(reply)}`);
}
return reply;
} catch (err: any) {
this.logger.error(`${provider.name} 调用失败: ${err.message}`);
return this.fallback(messages);
}
}
return this.fallback(messages);
}
private fallback(messages: ChatMessage[]): string {
const lastMsg = messages[messages.length - 1]?.content || '';
const mockReplies: Record<string, string> = {
'你好': '你好!我是宇之然 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}`;
}
getRegisteredProviders(): string[] {
return Array.from(this.providers.keys());
}
}
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<string> {
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;
return data.choices[0].message.content;
}
}