| | import Koa from 'koa'; |
| | import KoaRouter from 'koa-router'; |
| | import koaRange from 'koa-range'; |
| | import koaCors from "koa2-cors"; |
| | import koaBody from 'koa-body'; |
| | import _ from 'lodash'; |
| |
|
| | import Exception from './exceptions/Exception.ts'; |
| | import Request from './request/Request.ts'; |
| | import Response from './response/Response.js'; |
| | import FailureBody from './response/FailureBody.ts'; |
| | import EX from './consts/exceptions.ts'; |
| | import logger from './logger.ts'; |
| | import config from './config.ts'; |
| |
|
| | class Server { |
| |
|
| | app; |
| | router; |
| | koaBodyMiddleware; |
| |
|
| | constructor() { |
| | this.app = new Koa(); |
| | this.app.use(koaCors()); |
| | |
| | this.app.use(koaRange); |
| | this.router = new KoaRouter({ prefix: config.service.urlPrefix }); |
| |
|
| | |
| | this.koaBodyMiddleware = koaBody({ |
| | multipart: true, |
| | formidable: { |
| | maxFileSize: 100 * 1024 * 1024, |
| | keepExtensions: true, |
| | }, |
| | formLimit: '100mb', |
| | jsonLimit: '100mb', |
| | textLimit: '100mb', |
| | parsedMethods: ['POST', 'PUT', 'PATCH'], |
| | }); |
| |
|
| | |
| | this.app.use(async (ctx: any, next: Function) => { |
| | if(ctx.request.type === "application/xml" || ctx.request.type === "application/ssml+xml") |
| | ctx.req.headers["content-type"] = "text/xml"; |
| | try { await next() } |
| | catch (err) { |
| | logger.error(err); |
| | const failureBody = new FailureBody(err); |
| | new Response(failureBody).injectTo(ctx); |
| | } |
| | }); |
| | |
| | this.app.use(async (ctx: any, next: Function) => { |
| | |
| | if (ctx.is('multipart')) { |
| | await next(); |
| | return; |
| | } |
| | if (ctx.is('application/json') && ['POST', 'PUT', 'PATCH'].includes(ctx.method)) { |
| | logger.debug('开始自定义 JSON 解析'); |
| | const chunks: Buffer[] = []; |
| |
|
| | await new Promise((resolve, reject) => { |
| | ctx.req.on('data', (chunk: Buffer) => { |
| | chunks.push(chunk); |
| | }); |
| |
|
| | ctx.req.on('end', () => { |
| | resolve(null); |
| | }); |
| |
|
| | ctx.req.on('error', reject); |
| | }); |
| |
|
| | const body = Buffer.concat(chunks).toString('utf8'); |
| |
|
| | |
| | let cleanedBody = body |
| | .replace(/\r\n/g, '\n') |
| | .replace(/\r/g, '\n') |
| | .replace(/\u00A0/g, ' ') |
| | .replace(/[\u2000-\u200B]/g, ' ') |
| | .replace(/\uFEFF/g, '') |
| | .trim(); |
| |
|
| | const parsedBody = JSON.parse(cleanedBody); |
| |
|
| | logger.debug('JSON 解析成功,跳过 koa-body'); |
| |
|
| | ctx.request.body = parsedBody; |
| | ctx.request.rawBody = cleanedBody; |
| |
|
| | |
| | ctx._jsonProcessed = true; |
| | } |
| | await next(); |
| | }); |
| |
|
| | |
| | this.app.use(async (ctx: any, next: Function) => { |
| | if (!ctx._jsonProcessed) { |
| | await this.koaBodyMiddleware(ctx, next); |
| | } else { |
| | await next(); |
| | } |
| | }); |
| | this.app.on("error", (err: any) => { |
| | |
| | if (["ECONNRESET", "ECONNABORTED", "EPIPE", "ECANCELED"].includes(err.code)) return; |
| | logger.error(err); |
| | }); |
| | logger.success("Server initialized"); |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | |
| | attachRoutes(routes: any[]) { |
| | routes.forEach((route: any) => { |
| | const prefix = route.prefix || ""; |
| | for (let method in route) { |
| | if(method === "prefix") continue; |
| | if (!_.isObject(route[method])) { |
| | logger.warn(`Router ${prefix} ${method} invalid`); |
| | continue; |
| | } |
| | for (let uri in route[method]) { |
| | this.router[method](`${prefix}${uri}`, async ctx => { |
| | const { request, response } = await this.#requestProcessing(ctx, route[method][uri]); |
| | if(response != null && config.system.requestLog) |
| | logger.info(`<- ${request.method} ${request.url} ${response.time - request.time}ms`); |
| | }); |
| | } |
| | } |
| | logger.info(`Route ${config.service.urlPrefix || ""}${prefix} attached`); |
| | }); |
| | this.app.use(this.router.routes()); |
| | this.app.use((ctx: any) => { |
| | const request = new Request(ctx); |
| | logger.debug(`-> ${ctx.request.method} ${ctx.request.url} request is not supported - ${request.remoteIP || "unknown"}`); |
| | |
| | |
| | const message = `[请求有误]: 正确请求为 POST -> /v1/chat/completions,当前请求为 ${ctx.request.method} -> ${ctx.request.url} 请纠正`; |
| | logger.warn(message); |
| | const failureBody = new FailureBody(new Error(message)); |
| | const response = new Response(failureBody); |
| | response.injectTo(ctx); |
| | if(config.system.requestLog) |
| | logger.info(`<- ${request.method} ${request.url} ${response.time - request.time}ms`); |
| | }); |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | #requestProcessing(ctx: any, routeFn: Function): Promise<any> { |
| | return new Promise(resolve => { |
| | const request = new Request(ctx); |
| | try { |
| | if(config.system.requestLog) |
| | logger.info(`-> ${request.method} ${request.url}`); |
| | routeFn(request) |
| | .then(response => { |
| | try { |
| | if(!Response.isInstance(response)) { |
| | const _response = new Response(response); |
| | _response.injectTo(ctx); |
| | return resolve({ request, response: _response }); |
| | } |
| | response.injectTo(ctx); |
| | resolve({ request, response }); |
| | } |
| | catch(err) { |
| | logger.error(err); |
| | const failureBody = new FailureBody(err); |
| | const response = new Response(failureBody); |
| | response.injectTo(ctx); |
| | resolve({ request, response }); |
| | } |
| | }) |
| | .catch(err => { |
| | try { |
| | logger.error(err); |
| | const failureBody = new FailureBody(err); |
| | const response = new Response(failureBody); |
| | response.injectTo(ctx); |
| | resolve({ request, response }); |
| | } |
| | catch(err) { |
| | logger.error(err); |
| | const failureBody = new FailureBody(err); |
| | const response = new Response(failureBody); |
| | response.injectTo(ctx); |
| | resolve({ request, response }); |
| | } |
| | }); |
| | } |
| | catch(err) { |
| | logger.error(err); |
| | const failureBody = new FailureBody(err); |
| | const response = new Response(failureBody); |
| | response.injectTo(ctx); |
| | resolve({ request, response }); |
| | } |
| | }); |
| | } |
| |
|
| | |
| | |
| | |
| | async listen() { |
| | const host = config.service.host; |
| | const port = config.service.port; |
| | await Promise.all([ |
| | new Promise((resolve, reject) => { |
| | if(host === "0.0.0.0" || host === "localhost" || host === "127.0.0.1") |
| | return resolve(null); |
| | this.app.listen(port, "localhost", err => { |
| | if(err) return reject(err); |
| | resolve(null); |
| | }); |
| | }), |
| | new Promise((resolve, reject) => { |
| | this.app.listen(port, host, err => { |
| | if(err) return reject(err); |
| | resolve(null); |
| | }); |
| | }) |
| | ]); |
| | logger.success(`Server listening on port ${port} (${host})`); |
| | } |
| |
|
| | } |
| |
|
| | export default new Server(); |