|
|
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(); |