linux / app.js
dakaca's picture
Create app.js
8c25279
const express = require('express')
const fetch = require('cross-fetch')
const app = express()
var multer = require('multer');
var forms = multer({limits: { fieldSize: 10*1024*1024 }});
app.use(forms.array());
const cors = require('cors');
app.use(cors());
const bodyParser = require('body-parser')
app.use(bodyParser.json({limit : '50mb' }));
app.use(bodyParser.urlencoded({ extended: true }));
const tencentcloud = require("tencentcloud-sdk-nodejs");
const TmsClient = tencentcloud.tms.v20201229.Client;
const clientConfig = {
credential: {
secretId: process.env.TENCENT_CLOUD_SID,
secretKey: process.env.TENCENT_CLOUD_SKEY,
},
region: process.env.TENCENT_CLOUD_AP||"ap-singapore",
profile: {
httpProfile: {
endpoint: "tms.tencentcloudapi.com",
},
},
};
const mdClient = process.env.TENCENT_CLOUD_SID && process.env.TENCENT_CLOUD_SKEY ? new TmsClient(clientConfig) : false;
const controller = new AbortController();
app.all(`*`, async (req, res) => {
if(req.originalUrl) req.url = req.originalUrl;
let url = `https://api.openai.com${req.url}`;
// 从 header 中取得 Authorization': 'Bearer 后的 token
const token = req.headers.authorization?.split(' ')[1];
if( !token ) return res.status(403).send('Forbidden');
const openai_key = process.env.OPENAI_KEY||token.split(':')[0];
if( !openai_key ) return res.status(403).send('Forbidden');
if( openai_key.startsWith("fk") ) url = url.replaceAll( "api.openai.com", "openai.api2d.net" );
const proxy_key = token.split(':')[1]||"";
if( process.env.PROXY_KEY && proxy_key !== process.env.PROXY_KEY )
return res.status(403).send('Forbidden');
// console.log( req );
const { moderation, moderation_level, ...restBody } = req.body;
let sentence = "";
// 建立一个句子缓冲区
let sentence_buffer = [];
let processing = false;
let processing_stop = false;
async function process_buffer(res)
{
if( processing_stop )
{
console.log("processing_stop",processing_stop);
return false;
}
console.log("句子缓冲区" + new Date(), sentence_buffer);
// 处理句子缓冲区
if( processing )
{
// 有正在处理的,1秒钟后重试
console.log("有正在处理的,1秒钟后重试");
setTimeout( () => process_buffer(res), 1000 );
return false;
}
processing = true;
const sentence = sentence_buffer.shift();
console.log("取出句子", sentence);
if( sentence )
{
if( sentence === '[DONE]' )
{
console.log("[DONE]", "结束输出");
res.write("data: "+sentence+"\n\n" );
processing = false;
res.end();
return true;
}else
{
// 开始对句子进行审核
let data_array = JSON.parse(sentence);
console.log("解析句子数据为array",data_array);
const sentence_content = data_array.choices[0]?.delta?.content;
console.log("sentence_content", sentence_content);
if( sentence_content )
{
const params = {"Content": Buffer.from(sentence_content).toString('base64')};
const md_result = await mdClient.TextModeration(params);
// console.log("审核结果", md_result);
let md_check = moderation_level == 'high' ? md_result.Suggestion != 'Pass' : md_result.Suggestion == 'Block';
if( md_check )
{
// 终止输出
console.log("审核不通过", sentence_content, md_result);
let forbidden_array = data_array;
forbidden_array.choices[0].delta.content = "这个话题不适合讨论,换个话题吧。";
res.write("data: "+JSON.stringify(forbidden_array)+"\n\n" );
res.write("data: [DONE]\n\n" );
res.end();
controller.abort();
processing = false;
processing_stop = true;
return false;
}else
{
console.log("审核通过", sentence_content);
res.write("data: "+sentence+"\n\n" );
processing = false;
console.log("processing",processing);
return true;
}
}
}
}else
{
// console.log("句子缓冲区为空");
}
processing = false;
}
const options = {
method: req.method,
timeout: process.env.TIMEOUT||30000,
signal: controller.signal,
headers: {
'Content-Type': 'application/json; charset=utf-8',
'Authorization': 'Bearer '+ openai_key,
},
onMessage: async (data) => {
// console.log(data);
if( data === '[DONE]' )
{
sentence_buffer.push(data);
await process_buffer(res);
}else
{
if( moderation && mdClient )
{
try {
let data_array = JSON.parse(data);
const char = data_array.choices[0]?.delta?.content;
if( char ) sentence += char;
// console.log("sentence",sentence );
if( char == '。' || char == '?' || char == '!' || char == "\n" )
{
// 将 sentence 送审
console.log("遇到句号,将句子放入缓冲区", sentence);
data_array.choices[0].delta.content = sentence;
sentence = "";
sentence_buffer.push(JSON.stringify(data_array));
await process_buffer(res);
}
} catch (error) {
// 因为开头已经处理的了 [DONE] 的情况,这里应该不会出现无法解析json的情况
console.log( "error", error );
}
}else
{
// 如果没有文本审核参数或者设置,直接输出
res.write("data: "+data+"\n\n" );
}
}
}
};
if( req.method.toLocaleLowerCase() === 'post' && req.body ) options.body = JSON.stringify(restBody);
// console.log({url, options});
try {
// 如果是 chat completion 和 text completion,使用 SSE
if( (req.url.startsWith('/v1/completions') || req.url.startsWith('/v1/chat/completions')) && req.body.stream ) {
console.log("使用 SSE");
const response = await myFetch(url, options);
if( response.ok )
{
// write header
res.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
});
const { createParser } = await import("eventsource-parser");
const parser = createParser((event) => {
// console.log(event);
if (event.type === "event") {
options.onMessage(event.data);
}
});
if (!response.body.getReader) {
const body = response.body;
if (!body.on || !body.read) {
throw new error('unsupported "fetch" implementation');
}
body.on("readable", () => {
let chunk;
while (null !== (chunk = body.read())) {
// console.log(chunk.toString());
parser.feed(chunk.toString());
}
});
} else {
for await (const chunk of streamAsyncIterable(response.body)) {
const str = new TextDecoder().decode(chunk);
parser.feed(str);
}
}
}else
{
const body = await response.text();
res.status(response.status).send(body);
}
}else
{
console.log("使用 fetch");
const response = await myFetch(url, options);
// console.log(response);
const data = await response.json();
// 审核结果
if( moderation && mdClient )
{
const params = {"Content": Buffer.from(data.choices[0].message.content).toString('base64')};
const md_result = await mdClient.TextModeration(params);
// console.log("审核结果", md_result);
let md_check = moderation_level == 'high' ? md_result.Suggestion != 'Pass' : md_result.Suggestion == 'Block';
if( md_check )
{
// 终止输出
console.log("审核不通过", data.choices[0].message.content, md_result);
data.choices[0].message.content = "这个话题不适合讨论,换个话题吧。";
}else
{
console.log("审核通过", data.choices[0].message.content);
}
}
res.json(data);
}
} catch (error) {
console.error(error);
res.status(500).json({"error":error.toString()});
}
})
async function* streamAsyncIterable(stream) {
const reader = stream.getReader();
try {
while (true) {
const { done, value } = await reader.read();
if (done) {
return;
}
yield value;
}
} finally {
reader.releaseLock();
}
}
async function myFetch(url, options) {
const {timeout, ...fetchOptions} = options;
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeout||30000)
const res = await fetch(url, {...fetchOptions,signal:controller.signal});
clearTimeout(timeoutId);
return res;
}
// Error handler
app.use(function(err, req, res, next) {
console.error(err)
res.status(500).send('Internal Serverless Error')
})
const port = process.env.PORT||7860;
app.listen(port, () => {
console.log(`Server start on http://localhost:${port}`);
})