mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-18 17:21:23 +02:00
✨ feat(api-gateway): add Swagger, admin endpoints, and scheduler
- Add Swagger/OpenAPI documentation at /docs endpoint - Add admin module for system-wide API key management - Add scheduler for monthly credit reset and usage cleanup - Add Docker Compose entry for Mac Mini deployment - Document all endpoints with descriptions and examples
This commit is contained in:
parent
4b322f59b1
commit
fc0ed636fc
21 changed files with 1059 additions and 1 deletions
|
|
@ -9,6 +9,15 @@ import {
|
|||
Res,
|
||||
Query,
|
||||
} from '@nestjs/common';
|
||||
import {
|
||||
ApiTags,
|
||||
ApiSecurity,
|
||||
ApiOperation,
|
||||
ApiResponse,
|
||||
ApiConsumes,
|
||||
ApiBody,
|
||||
ApiHeader,
|
||||
} from '@nestjs/swagger';
|
||||
import { FileInterceptor } from '@nestjs/platform-express';
|
||||
import { Response } from 'express';
|
||||
import { ApiKeyGuard } from '../guards/api-key.guard';
|
||||
|
|
@ -26,6 +35,12 @@ import {
|
|||
import { SttProxyService, TranscribeRequestDto } from './services/stt-proxy.service';
|
||||
import { TtsProxyService, SynthesizeRequestDto } from './services/tts-proxy.service';
|
||||
|
||||
@ApiSecurity('api-key')
|
||||
@ApiHeader({
|
||||
name: 'X-API-Key',
|
||||
description: 'Your API key (sk_live_xxx or sk_test_xxx)',
|
||||
required: true,
|
||||
})
|
||||
@Controller('v1')
|
||||
@UseGuards(ApiKeyGuard, RateLimitGuard, CreditsGuard)
|
||||
@UseInterceptors(UsageTrackingInterceptor)
|
||||
|
|
@ -39,21 +54,106 @@ export class ProxyController {
|
|||
// === SEARCH ===
|
||||
|
||||
@Post('search')
|
||||
@ApiTags('Search')
|
||||
@ApiOperation({
|
||||
summary: 'Web search',
|
||||
description: 'Search the web using multiple search engines. Costs 1 credit per request.',
|
||||
})
|
||||
@ApiBody({
|
||||
schema: {
|
||||
type: 'object',
|
||||
required: ['query'],
|
||||
properties: {
|
||||
query: { type: 'string', example: 'quantum computing' },
|
||||
options: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
categories: {
|
||||
type: 'array',
|
||||
items: { type: 'string' },
|
||||
example: ['general', 'science'],
|
||||
},
|
||||
engines: { type: 'array', items: { type: 'string' }, example: ['google', 'bing'] },
|
||||
language: { type: 'string', example: 'en' },
|
||||
limit: { type: 'number', example: 10 },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
@ApiResponse({ status: 200, description: 'Search results' })
|
||||
@ApiResponse({ status: 429, description: 'Rate limit exceeded' })
|
||||
@ApiResponse({ status: 402, description: 'Insufficient credits' })
|
||||
async search(@Body() body: SearchRequestDto, @ApiKeyParam() apiKey: ApiKeyData) {
|
||||
return this.searchProxy.search(body);
|
||||
}
|
||||
|
||||
@Get('search/engines')
|
||||
@ApiTags('Search')
|
||||
@ApiOperation({
|
||||
summary: 'Get available search engines',
|
||||
description: 'Returns a list of available search engines and categories. Free endpoint.',
|
||||
})
|
||||
@ApiResponse({ status: 200, description: 'List of search engines' })
|
||||
async getEngines() {
|
||||
return this.searchProxy.getEngines();
|
||||
}
|
||||
|
||||
@Post('extract')
|
||||
@ApiTags('Search')
|
||||
@ApiOperation({
|
||||
summary: 'Extract content from URL',
|
||||
description: 'Extracts main content from a webpage. Costs 1 credit per request.',
|
||||
})
|
||||
@ApiBody({
|
||||
schema: {
|
||||
type: 'object',
|
||||
required: ['url'],
|
||||
properties: {
|
||||
url: { type: 'string', example: 'https://example.com/article' },
|
||||
options: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
includeMarkdown: { type: 'boolean', example: true },
|
||||
maxLength: { type: 'number', example: 5000 },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
@ApiResponse({ status: 200, description: 'Extracted content' })
|
||||
async extract(@Body() body: ExtractRequestDto) {
|
||||
return this.searchProxy.extract(body);
|
||||
}
|
||||
|
||||
@Post('extract/bulk')
|
||||
@ApiTags('Search')
|
||||
@ApiOperation({
|
||||
summary: 'Bulk extract content',
|
||||
description: 'Extracts content from multiple URLs (max 20). Costs 1 credit per URL.',
|
||||
})
|
||||
@ApiBody({
|
||||
schema: {
|
||||
type: 'object',
|
||||
required: ['urls'],
|
||||
properties: {
|
||||
urls: {
|
||||
type: 'array',
|
||||
items: { type: 'string' },
|
||||
example: ['https://example.com/article1', 'https://example.com/article2'],
|
||||
},
|
||||
options: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
includeMarkdown: { type: 'boolean', example: true },
|
||||
maxLength: { type: 'number', example: 5000 },
|
||||
},
|
||||
},
|
||||
concurrency: { type: 'number', example: 5 },
|
||||
},
|
||||
},
|
||||
})
|
||||
@ApiResponse({ status: 200, description: 'Extracted content from all URLs' })
|
||||
async bulkExtract(@Body() body: BulkExtractRequestDto) {
|
||||
return this.searchProxy.bulkExtract(body);
|
||||
}
|
||||
|
|
@ -61,17 +161,49 @@ export class ProxyController {
|
|||
// === STT ===
|
||||
|
||||
@Post('stt/transcribe')
|
||||
@ApiTags('STT')
|
||||
@ApiOperation({
|
||||
summary: 'Transcribe audio to text',
|
||||
description:
|
||||
'Converts audio file to text. Costs 10 credits per minute of audio. Supports WAV, MP3, OGG, FLAC.',
|
||||
})
|
||||
@ApiConsumes('multipart/form-data')
|
||||
@ApiBody({
|
||||
schema: {
|
||||
type: 'object',
|
||||
required: ['file'],
|
||||
properties: {
|
||||
file: { type: 'string', format: 'binary', description: 'Audio file' },
|
||||
language: { type: 'string', example: 'en', description: 'Language code (optional)' },
|
||||
model: { type: 'string', example: 'base', description: 'Model name (optional)' },
|
||||
},
|
||||
},
|
||||
})
|
||||
@ApiResponse({ status: 200, description: 'Transcription result' })
|
||||
@ApiResponse({ status: 400, description: 'Invalid audio file' })
|
||||
@UseInterceptors(FileInterceptor('file'))
|
||||
async transcribe(@UploadedFile() file: Express.Multer.File, @Body() body: TranscribeRequestDto) {
|
||||
return this.sttProxy.transcribe(file, body);
|
||||
}
|
||||
|
||||
@Get('stt/models')
|
||||
@ApiTags('STT')
|
||||
@ApiOperation({
|
||||
summary: 'Get available STT models',
|
||||
description: 'Returns a list of available speech-to-text models. Free endpoint.',
|
||||
})
|
||||
@ApiResponse({ status: 200, description: 'List of STT models' })
|
||||
async getSttModels() {
|
||||
return this.sttProxy.getModels();
|
||||
}
|
||||
|
||||
@Get('stt/languages')
|
||||
@ApiTags('STT')
|
||||
@ApiOperation({
|
||||
summary: 'Get supported languages',
|
||||
description: 'Returns a list of languages supported by STT. Free endpoint.',
|
||||
})
|
||||
@ApiResponse({ status: 200, description: 'List of supported languages' })
|
||||
async getSttLanguages() {
|
||||
return this.sttProxy.getLanguages();
|
||||
}
|
||||
|
|
@ -79,6 +211,34 @@ export class ProxyController {
|
|||
// === TTS ===
|
||||
|
||||
@Post('tts/synthesize')
|
||||
@ApiTags('TTS')
|
||||
@ApiOperation({
|
||||
summary: 'Synthesize text to speech',
|
||||
description:
|
||||
'Converts text to audio. Costs 1 credit per 1000 characters. Returns audio file (MP3, WAV, or OGG).',
|
||||
})
|
||||
@ApiBody({
|
||||
schema: {
|
||||
type: 'object',
|
||||
required: ['text'],
|
||||
properties: {
|
||||
text: { type: 'string', example: 'Hello, world! This is a test.' },
|
||||
voice: { type: 'string', example: 'en-US-1', description: 'Voice ID' },
|
||||
language: { type: 'string', example: 'en-US', description: 'Language code' },
|
||||
speed: { type: 'number', example: 1.0, minimum: 0.5, maximum: 2.0 },
|
||||
format: { type: 'string', enum: ['mp3', 'wav', 'ogg'], default: 'mp3' },
|
||||
},
|
||||
},
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: 'Audio file',
|
||||
content: {
|
||||
'audio/mpeg': { schema: { type: 'string', format: 'binary' } },
|
||||
'audio/wav': { schema: { type: 'string', format: 'binary' } },
|
||||
'audio/ogg': { schema: { type: 'string', format: 'binary' } },
|
||||
},
|
||||
})
|
||||
async synthesize(@Body() body: SynthesizeRequestDto, @Res() res: Response) {
|
||||
const audio = await this.ttsProxy.synthesize(body);
|
||||
|
||||
|
|
@ -92,11 +252,23 @@ export class ProxyController {
|
|||
}
|
||||
|
||||
@Get('tts/voices')
|
||||
@ApiTags('TTS')
|
||||
@ApiOperation({
|
||||
summary: 'Get available voices',
|
||||
description: 'Returns a list of available TTS voices. Free endpoint.',
|
||||
})
|
||||
@ApiResponse({ status: 200, description: 'List of available voices' })
|
||||
async getTtsVoices() {
|
||||
return this.ttsProxy.getVoices();
|
||||
}
|
||||
|
||||
@Get('tts/languages')
|
||||
@ApiTags('TTS')
|
||||
@ApiOperation({
|
||||
summary: 'Get supported TTS languages',
|
||||
description: 'Returns a list of languages supported by TTS. Free endpoint.',
|
||||
})
|
||||
@ApiResponse({ status: 200, description: 'List of supported languages' })
|
||||
async getTtsLanguages() {
|
||||
return this.ttsProxy.getLanguages();
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue