From cd0ef01c7c56f9d2993a7af16bb935a81c366b07 Mon Sep 17 00:00:00 2001 From: TradeMate Dev Date: Tue, 23 Jun 2026 21:23:21 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B2=99=E7=9B=92=E6=A8=A1=E5=9E=8B?= =?UTF-8?q?=E5=90=88=E8=A7=84=E4=BC=98=E5=8C=96=20=E2=80=94=20=E7=A7=BB?= =?UTF-8?q?=E9=99=A4=E5=A2=83=E5=A4=96=E6=A8=A1=E5=9E=8B=E5=BC=95=E7=94=A8?= =?UTF-8?q?=EF=BC=8C=E6=96=B0=E5=A2=9E=20GET=20/sandbox/models=20=E6=8E=A5?= =?UTF-8?q?=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - MODEL_CATALOG 仅保留国内已备案模型(DeepSeek/SenseNova) - chat()/chatStream() modelMap 仅路由到国内 provider - provider 命名明确化:"OpenAI 兼容接口" → "硅基流动 / DeepSeek" - 新增 getSandboxModels() 动态返回可用模型列表 - sandbox 控制器拆分 auth 级别,GET /sandbox/models 无需认证 - 新增 getAvailableModels() 供前端调用 Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- backend/src/modules/ai/ai-gateway.service.ts | 56 ++++++++++--------- .../src/modules/sandbox/sandbox.controller.ts | 25 ++++++++- .../src/modules/sandbox/sandbox.service.ts | 17 ++++++ 3 files changed, 69 insertions(+), 29 deletions(-) diff --git a/backend/src/modules/ai/ai-gateway.service.ts b/backend/src/modules/ai/ai-gateway.service.ts index 9ebb1ca..e58f350 100755 --- a/backend/src/modules/ai/ai-gateway.service.ts +++ b/backend/src/modules/ai/ai-gateway.service.ts @@ -24,15 +24,17 @@ interface AIProvider { chatStream?(messages: ChatMessage[], options?: ChatOptions): AsyncIterable; } +// 仅收录沙箱实际可用的国内已备案大模型 const MODEL_CATALOG: Record = { - 'general': { id: 'general', provider: 'OpenAI 兼容', capabilities: ['chat', 'code'], contextWindow: 8192 }, - 'openai': { id: 'openai', provider: 'OpenAI 兼容', capabilities: ['chat', 'code'], contextWindow: 8192 }, - 'gpt-3.5': { id: 'gpt-3.5', provider: 'OpenAI', capabilities: ['chat', 'code'], contextWindow: 16384 }, - 'gpt-4': { id: 'gpt-4', provider: 'OpenAI', capabilities: ['chat', 'code', 'vision'], contextWindow: 32768 }, - 'deepseek-v4-flash': { id: 'deepseek-v4-flash', provider: '商汤科技', capabilities: ['chat', 'code'], contextWindow: 32768 }, + '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 { @@ -45,19 +47,21 @@ export class AIGatewayService { } 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 || 'gpt-3.5-turbo'; - this.providers.set('openai', new OpenAICompatibleProvider( + 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, - 'OpenAI 兼容接口', + '硅基流动 / DeepSeek', )); - this.logger.log(`OpenAI 兼容接口已注册: ${defaultModel}`); + this.logger.log(`DeepSeek(通过国内渠道)已注册: ${defaultModel}`); + this.markAvailable('deepseek-v4-flash'); } if (process.env.SENSENOVA_API_KEY) { @@ -73,25 +77,27 @@ export class AIGatewayService { 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 = { - 'general': 'openai', - 'openai': 'openai', - 'gpt-3.5': 'openai', - 'gpt-4': 'openai', - 'longcat': 'openai', - 'meituan/longcat-flash-lite': 'openai', 'deepseek-v4-flash': 'deepseek-v4-flash', 'sensenova-6.7-flash-lite': 'sensenova-6.7-flash-lite', - }; - const providerKey = modelMap[model.toLowerCase()] || (model.includes('/') ? 'openai' : model); + 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 }; @@ -117,18 +123,11 @@ export class AIGatewayService { async *chatStream(model: string, messages: ChatMessage[], options?: ChatOptions): AsyncIterable { const modelMap: Record = { - 'general': 'openai', - 'openai': 'openai', - 'gpt-3.5': 'openai', - 'gpt-4': 'openai', - 'longcat': 'openai', - 'meituan/longcat-flash-lite': 'openai', 'deepseek-v4-flash': 'deepseek-v4-flash', 'sensenova-6.7-flash-lite': 'sensenova-6.7-flash-lite', - }; - const providerKey = modelMap[model.toLowerCase()] || (model.includes('/') ? 'openai' : model); + 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 }; @@ -201,6 +200,11 @@ export class AIGatewayService { getRegisteredProviders(): string[] { return Array.from(this.providers.keys()); } + + /** 返回沙箱前端可选的模型列表(仅返回有 provider 注册的模型) */ + getSandboxModels() { + return SANDBOX_MODELS.filter(m => m.available); + } } class OpenAICompatibleProvider implements AIProvider { diff --git a/backend/src/modules/sandbox/sandbox.controller.ts b/backend/src/modules/sandbox/sandbox.controller.ts index dde8f7d..516c101 100755 --- a/backend/src/modules/sandbox/sandbox.controller.ts +++ b/backend/src/modules/sandbox/sandbox.controller.ts @@ -7,13 +7,18 @@ import { Response } from 'express'; @ApiTags('AI沙箱') @Controller('sandbox') -@UseGuards(AuthGuard('jwt')) -@ApiBearerAuth() export class SandboxController { constructor(private sandboxService: SandboxService) {} + @Get('models') + async getModels() { + return { items: this.sandboxService.getAvailableModels() }; + } + @Post('chat') - @ApiBody({ schema: { example: { conversationId: 'uuid', model: 'general', messages: [{ role: 'user', content: 'hi' }], images: ['https://example.com/img.png'], temperature: 0.7, top_p: 1, max_tokens: 2000, stream: false } } }) + @UseGuards(AuthGuard('jwt')) + @ApiBearerAuth() + @ApiBody({ schema: { example: { conversationId: 'uuid', model: 'deepseek-v4-flash', messages: [{ role: 'user', content: 'hi' }], images: ['https://example.com/img.png'], temperature: 0.7, top_p: 1, max_tokens: 2000, stream: false } } }) async chat(@Req() req: any, @Body() body: { conversationId?: string; model: string; messages: { role: string; content: string }[]; images?: string[]; stream?: boolean } & ChatOptions, @Res() res: Response) { if (body.stream) { res.setHeader('Content-Type', 'text/event-stream'); @@ -35,36 +40,50 @@ export class SandboxController { } @Get('sessions') + @UseGuards(AuthGuard('jwt')) + @ApiBearerAuth() async sessions(@Req() req: any, @Query() query: { page?: number; pageSize?: number; search?: string }) { return this.sandboxService.getSessions(req.user.userId, query); } @Get('sessions/:id') + @UseGuards(AuthGuard('jwt')) + @ApiBearerAuth() async getSession(@Req() req: any, @Param('id', ParseIntPipe) id: number) { return this.sandboxService.getSession(req.user.userId, id); } @Patch('sessions/:id/feedback') + @UseGuards(AuthGuard('jwt')) + @ApiBearerAuth() async setFeedback(@Req() req: any, @Param('id', ParseIntPipe) id: number, @Body() body: { feedback: 'LIKE' | 'DISLIKE' | null }) { return this.sandboxService.setFeedback(req.user.userId, id, body.feedback); } @Delete('sessions/:id') + @UseGuards(AuthGuard('jwt')) + @ApiBearerAuth() async deleteSession(@Req() req: any, @Param('id', ParseIntPipe) id: number) { return this.sandboxService.deleteSession(req.user.userId, id); } @Get('quota') + @UseGuards(AuthGuard('jwt')) + @ApiBearerAuth() async quota(@Req() req: any) { return this.sandboxService.getQuota(req.user.userId); } @Post('sessions/:id/share') + @UseGuards(AuthGuard('jwt')) + @ApiBearerAuth() async share(@Req() req: any, @Param('id', ParseIntPipe) id: number) { return this.sandboxService.generateShareToken(req.user.userId, id); } @Patch('sessions/:id/rename') + @UseGuards(AuthGuard('jwt')) + @ApiBearerAuth() async rename(@Req() req: any, @Param('id', ParseIntPipe) id: number, @Body() body: { title: string }) { return this.sandboxService.renameSession(req.user.userId, id, body.title); } diff --git a/backend/src/modules/sandbox/sandbox.service.ts b/backend/src/modules/sandbox/sandbox.service.ts index 798b13e..430f48c 100755 --- a/backend/src/modules/sandbox/sandbox.service.ts +++ b/backend/src/modules/sandbox/sandbox.service.ts @@ -3,6 +3,13 @@ import { PrismaService } from '../../prisma/prisma.service'; import { AIGatewayService, ChatOptions } from '../ai/ai-gateway.service'; import { randomUUID, createHmac } from 'crypto'; +export interface SandboxModelInfo { + id: string; + label: string; + provider: string; + desc: string; +} + interface StreamResult { type: 'text' | 'done' | 'error'; content?: string; @@ -288,6 +295,16 @@ export class SandboxService { }; } + /** 返回沙箱中可用的模型列表 */ + getAvailableModels(): SandboxModelInfo[] { + return this.aiGateway.getSandboxModels().map(m => ({ + id: m.id, + label: m.label, + provider: m.provider, + desc: m.desc, + })); + } + async generateShareToken(userId: number, sessionId: number) { const session = await this.prisma.sandboxSession.findFirst({ where: { id: sessionId, userId },