import type { NextRequest } from 'next/server'; import type { IService } from 'soma-core'; import { AiAssistantRuntime } from '../../../domain/ai-assistant/AiAssistantRuntime'; import { AiApiError, AiErrorCodes } from '../../../domain/ai-shared/AiError'; import { aiErrorResponse } from '../../../domain/ai-shared/AiResponse'; import { AI_REQUEST_MAX_BYTES, readJsonWithLimit, RequestBodyTooLargeError, } from '../../common/RequestBodyLimit'; import { callerKey, parseRateLimit, sharedRateLimiter } from '../../common/RateLimiter'; import { runService } from '../../common/ServiceRuntime'; import { resolveTrustedRequestContext } from '../../common/TrustedRequestContext'; import { readRequestOrigin } from '../../common/RequestOrigin'; export default class ChatService implements IService { async post(req: NextRequest): Promise { return runService(async () => { try { // 对话同样按调用方限流(缺省 60 次/分钟)——它也是每次都打上游 const rule = parseRateLimit(process.env.SOMA_AI_CHAT_RATE, { limit: 60, windowMs: 60_000 }); const actor = await resolveTrustedRequestContext(req); const decision = await sharedRateLimiter.hit(`ai-chat:${callerKey(actor)}`, rule); if (!decision.allowed) { const rejected = aiErrorResponse(new AiApiError( AiErrorCodes.AI_RATE_LIMITED, `对话请求过于频繁,请 ${decision.retryAfterSeconds} 秒后重试`, )); return new Response(rejected.body, { status: 429, headers: { ...Object.fromEntries(rejected.headers), 'retry-after': String(decision.retryAfterSeconds), }, }); } // 合法上界是 80 条 × 32KB(AI_LIMITS),但那是解析之后才判的; // 这一道在读之前把内存占用钉住。 let body: unknown; try { body = await readJsonWithLimit(req, AI_REQUEST_MAX_BYTES); } catch (error) { throw error instanceof RequestBodyTooLargeError ? new AiApiError(AiErrorCodes.AI_CHAT_INPUT_INVALID, error.message) : new AiApiError(AiErrorCodes.AI_CHAT_INPUT_INVALID, '请求 JSON 不合法'); } if (body === null) { throw new AiApiError(AiErrorCodes.AI_CHAT_INPUT_INVALID, '请求 JSON 不合法'); } const runtime = AiAssistantRuntime.getInstance(); const request = await runtime.validateRequest(body); // origin 给声明式工具自调宿主接口用:同源、同权、同样过入口网关。 return await runtime.createChatResponse(request, req.signal, readRequestOrigin(req)); } catch (error) { const response = aiErrorResponse(error); if (error instanceof AiApiError && error.code === AiErrorCodes.AI_CHAT_INPUT_INVALID) { return new Response(response.body, { status: 400, headers: response.headers }); } return response; } }); } }