Spaces:
Sleeping
Sleeping
| import { Router, type Response } from 'express'; | |
| import { verifyApiKey } from '../middleware/api-auth.js'; | |
| import { AIService } from '../services/ai.service.js'; | |
| import { ConcurrencyService } from '../services/concurrency.service.js'; | |
| import db from '../lib/db.js'; | |
| const router = Router(); | |
| /** | |
| * 外部 API: Chat Completion (类似 OpenAI 接口) | |
| * POST /api/v1/chat/completions | |
| */ | |
| router.post('/chat/completions', verifyApiKey, async (req: any, res) => { | |
| const { model, messages, stream = false } = req.body; | |
| const userId = req.user.userId; | |
| // 1. 参数校验 | |
| if (!messages || !Array.isArray(messages)) { | |
| return res.status(400).json({ error: { message: "'messages' must be an array" } }); | |
| } | |
| // 2. 检查配额 | |
| try { | |
| const user = db.prepare('SELECT quota_remaining FROM users WHERE id = ?').get(userId) as any; | |
| if (!user || user.quota_remaining <= 0) { | |
| return res.status(402).json({ | |
| error: { | |
| message: "You have exceeded your current quota.", | |
| type: "insufficient_quota", | |
| code: "quota_exceeded" | |
| } | |
| }); | |
| } | |
| // 3. 执行 AI 调用 (仅提取最后一条用户消息) | |
| const lastUserMsg = messages.reverse().find((m: any) => m.role === 'user'); | |
| const query = lastUserMsg?.content || ''; | |
| if (!query) { | |
| return res.status(400).json({ error: { message: "No user message found" } }); | |
| } | |
| // 4. 调用内部服务 | |
| const result = await AIService.chatWithKnowledge(userId, query); | |
| // 5. 扣除额度 (简单起见,每次调用扣 1 点) | |
| db.prepare('UPDATE users SET quota_remaining = quota_remaining - 1 WHERE id = ?').run(userId); | |
| if (stream) { | |
| // 流式响应 (SSE) | |
| res.setHeader('Content-Type', 'text/event-stream'); | |
| res.setHeader('Cache-Control', 'no-cache'); | |
| res.setHeader('Connection', 'keep-alive'); | |
| for await (const chunk of result.stream) { | |
| const content = chunk.choices[0]?.delta?.content || ''; | |
| if (content) { | |
| const sseData = JSON.stringify({ | |
| id: `chatcmpl-${Date.now()}`, | |
| object: 'chat.completion.chunk', | |
| created: Math.floor(Date.now() / 1000), | |
| model: model || 'codex-ai-v1', | |
| choices: [{ delta: { content }, index: 0, finish_reason: null }] | |
| }); | |
| res.write(`data: ${sseData}\n\n`); | |
| } | |
| } | |
| res.write('data: [DONE]\n\n'); | |
| res.end(); | |
| } else { | |
| // 非流式响应 | |
| let fullContent = ''; | |
| for await (const chunk of result.stream) { | |
| fullContent += chunk.choices[0]?.delta?.content || ''; | |
| } | |
| res.json({ | |
| id: `chatcmpl-${Date.now()}`, | |
| object: 'chat.completion', | |
| created: Math.floor(Date.now() / 1000), | |
| model: model || 'codex-ai-v1', | |
| usage: { prompt_tokens: query.length, completion_tokens: fullContent.length, total_tokens: query.length + fullContent.length }, | |
| choices: [{ message: { role: 'assistant', content: fullContent }, finish_reason: 'stop', index: 0 }] | |
| }); | |
| } | |
| } catch (err: any) { | |
| res.status(500).json({ error: { message: err.message, type: "server_error" } }); | |
| } | |
| }); | |
| /** | |
| * 外部 API: 获取用户余额 | |
| * GET /api/v1/user/quota | |
| */ | |
| router.get('/user/quota', verifyApiKey, (req: any, res) => { | |
| const userId = req.user.userId; | |
| const user = db.prepare('SELECT email, plan, quota_remaining FROM users WHERE id = ?').get(userId); | |
| res.json(user); | |
| }); | |
| export default router; | |