perf: bill
This commit is contained in:
114
client/src/service/api/axios.ts
Normal file
114
client/src/service/api/axios.ts
Normal file
@@ -0,0 +1,114 @@
|
||||
import axios, { Method, InternalAxiosRequestConfig, AxiosResponse } from 'axios';
|
||||
|
||||
interface ConfigType {
|
||||
headers?: { [key: string]: string };
|
||||
hold?: boolean;
|
||||
timeout?: number;
|
||||
}
|
||||
interface ResponseDataType {
|
||||
code: number;
|
||||
message: string;
|
||||
data: any;
|
||||
}
|
||||
|
||||
/**
|
||||
* 请求开始
|
||||
*/
|
||||
function requestStart(config: InternalAxiosRequestConfig): InternalAxiosRequestConfig {
|
||||
if (config.headers) {
|
||||
// config.headers.Authorization = getToken();
|
||||
}
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
/**
|
||||
* 请求成功,检查请求头
|
||||
*/
|
||||
function responseSuccess(response: AxiosResponse<ResponseDataType>) {
|
||||
return response;
|
||||
}
|
||||
/**
|
||||
* 响应数据检查
|
||||
*/
|
||||
function checkRes(data: ResponseDataType) {
|
||||
if (data === undefined) {
|
||||
console.log('error->', data, 'data is empty');
|
||||
return Promise.reject('服务器异常');
|
||||
} else if (data.code < 200 || data.code >= 400) {
|
||||
return Promise.reject(data);
|
||||
}
|
||||
return data.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* 响应错误
|
||||
*/
|
||||
function responseError(err: any) {
|
||||
console.log('error->', '请求错误', err);
|
||||
|
||||
if (!err) {
|
||||
return Promise.reject({ message: '未知错误' });
|
||||
}
|
||||
if (typeof err === 'string') {
|
||||
return Promise.reject({ message: err });
|
||||
}
|
||||
return Promise.reject(err);
|
||||
}
|
||||
|
||||
/* 创建请求实例 */
|
||||
const instance = axios.create({
|
||||
timeout: 60000, // 超时时间
|
||||
headers: {
|
||||
'content-type': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
/* 请求拦截 */
|
||||
instance.interceptors.request.use(requestStart, (err) => Promise.reject(err));
|
||||
/* 响应拦截 */
|
||||
instance.interceptors.response.use(responseSuccess, (err) => Promise.reject(err));
|
||||
|
||||
function request(url: string, data: any, config: ConfigType, method: Method): any {
|
||||
/* 去空 */
|
||||
for (const key in data) {
|
||||
if (data[key] === null || data[key] === undefined) {
|
||||
delete data[key];
|
||||
}
|
||||
}
|
||||
|
||||
return instance
|
||||
.request({
|
||||
baseURL: `http://localhost:${process.env.PORT || 3000}/api`,
|
||||
url,
|
||||
method,
|
||||
data: ['POST', 'PUT'].includes(method) ? data : null,
|
||||
params: !['POST', 'PUT'].includes(method) ? data : null,
|
||||
...config // 用户自定义配置,可以覆盖前面的配置
|
||||
})
|
||||
.then((res) => checkRes(res.data))
|
||||
.catch((err) => responseError(err));
|
||||
}
|
||||
|
||||
/**
|
||||
* api请求方式
|
||||
* @param {String} url
|
||||
* @param {Any} params
|
||||
* @param {Object} config
|
||||
* @returns
|
||||
*/
|
||||
export function GET<T>(url: string, params = {}, config: ConfigType = {}): Promise<T> {
|
||||
return request(url, params, config, 'GET');
|
||||
}
|
||||
|
||||
export function POST<T>(url: string, data = {}, config: ConfigType = {}): Promise<T> {
|
||||
return request(url, data, config, 'POST');
|
||||
}
|
||||
|
||||
export function PUT<T>(url: string, data = {}, config: ConfigType = {}): Promise<T> {
|
||||
return request(url, data, config, 'PUT');
|
||||
}
|
||||
|
||||
export function DELETE<T>(url: string, data = {}, config: ConfigType = {}): Promise<T> {
|
||||
return request(url, data, config, 'DELETE');
|
||||
}
|
||||
@@ -93,6 +93,8 @@ export const moduleFetch = ({ url, data, res }: Props) =>
|
||||
event: sseResponseEventEnum.answer,
|
||||
data: JSON.stringify(data)
|
||||
});
|
||||
} else if (item.event === sseResponseEventEnum.error) {
|
||||
return reject(getErrText(data, '流响应错误'));
|
||||
}
|
||||
});
|
||||
read();
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
import { TrainingData } from '@/service/mongo';
|
||||
import { getApiKey } from '../utils/auth';
|
||||
import { OpenAiChatEnum } from '@/constants/model';
|
||||
import { pushSplitDataBill } from '@/service/events/pushBill';
|
||||
import { openaiAccountError } from '../errorCode';
|
||||
import { modelServiceToolMap } from '../utils/chat';
|
||||
import { ChatRoleEnum } from '@/constants/chat';
|
||||
import { BillTypeEnum } from '@/constants/user';
|
||||
import { BillSourceEnum } from '@/constants/user';
|
||||
import { pushDataToKb } from '@/pages/api/openapi/kb/pushData';
|
||||
import { TrainingModeEnum } from '@/constants/plugin';
|
||||
import { ERROR_ENUM } from '../errorCode';
|
||||
import { sendInform } from '@/pages/api/user/inform/send';
|
||||
import { authBalanceByUid } from '../utils/auth';
|
||||
import { axiosConfig, getOpenAIApi } from '../ai/openai';
|
||||
import { ChatCompletionRequestMessage } from 'openai';
|
||||
|
||||
const reduceQueue = () => {
|
||||
global.qaQueueLen = global.qaQueueLen > 0 ? global.qaQueueLen - 1 : 0;
|
||||
@@ -37,7 +38,8 @@ export async function generateQA(): Promise<any> {
|
||||
kbId: 1,
|
||||
prompt: 1,
|
||||
q: 1,
|
||||
source: 1
|
||||
source: 1,
|
||||
model: 1
|
||||
});
|
||||
|
||||
// task preemption
|
||||
@@ -51,54 +53,59 @@ export async function generateQA(): Promise<any> {
|
||||
userId = String(data.userId);
|
||||
const kbId = String(data.kbId);
|
||||
|
||||
// 余额校验并获取 openapi Key
|
||||
const { systemAuthKey } = await getApiKey({
|
||||
model: OpenAiChatEnum.GPT35,
|
||||
userId,
|
||||
mustPay: true
|
||||
});
|
||||
await authBalanceByUid(userId);
|
||||
|
||||
const startTime = Date.now();
|
||||
|
||||
const chatAPI = getOpenAIApi();
|
||||
|
||||
// 请求 chatgpt 获取回答
|
||||
const response = await Promise.all(
|
||||
[data.q].map((text) =>
|
||||
modelServiceToolMap
|
||||
.chatCompletion({
|
||||
model: OpenAiChatEnum.GPT3516k,
|
||||
apiKey: systemAuthKey,
|
||||
temperature: 0.8,
|
||||
messages: [
|
||||
{
|
||||
obj: ChatRoleEnum.System,
|
||||
value: `你是出题人.
|
||||
[data.q].map((text) => {
|
||||
const messages: ChatCompletionRequestMessage[] = [
|
||||
{
|
||||
role: 'system',
|
||||
content: `你是出题人.
|
||||
${data.prompt || '用户会发送一段长文本'}.
|
||||
从中选出 25 个问题和答案. 答案详细完整. 按格式回答: Q1:
|
||||
A1:
|
||||
Q2:
|
||||
A2:
|
||||
...`
|
||||
},
|
||||
{
|
||||
obj: 'Human',
|
||||
value: text
|
||||
}
|
||||
],
|
||||
stream: false
|
||||
})
|
||||
.then(({ totalTokens, responseText, responseMessages }) => {
|
||||
const result = formatSplitText(responseText); // 格式化后的QA对
|
||||
},
|
||||
{
|
||||
role: 'user',
|
||||
content: text
|
||||
}
|
||||
];
|
||||
return chatAPI
|
||||
.createChatCompletion(
|
||||
{
|
||||
model: data.model,
|
||||
temperature: 0.8,
|
||||
messages,
|
||||
stream: false
|
||||
},
|
||||
{
|
||||
timeout: 480000,
|
||||
...axiosConfig()
|
||||
}
|
||||
)
|
||||
.then((res) => {
|
||||
const answer = res.data.choices?.[0].message?.content;
|
||||
const totalTokens = res.data.usage?.total_tokens || 0;
|
||||
|
||||
const result = formatSplitText(answer || ''); // 格式化后的QA对
|
||||
console.log(`split result length: `, result.length);
|
||||
// 计费
|
||||
pushSplitDataBill({
|
||||
isPay: result.length > 0,
|
||||
userId: data.userId,
|
||||
type: BillTypeEnum.QA,
|
||||
textLen: responseMessages.map((item) => item.value).join('').length,
|
||||
totalTokens
|
||||
totalTokens,
|
||||
model: data.model,
|
||||
appName: 'QA 拆分'
|
||||
});
|
||||
return {
|
||||
rawContent: responseText,
|
||||
rawContent: answer,
|
||||
result
|
||||
};
|
||||
})
|
||||
@@ -106,8 +113,8 @@ A2:
|
||||
console.log('QA拆分错误');
|
||||
console.log(err.response?.status, err.response?.statusText, err.response?.data);
|
||||
return Promise.reject(err);
|
||||
})
|
||||
)
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
const responseList = response.map((item) => item.result).flat();
|
||||
@@ -120,6 +127,7 @@ A2:
|
||||
source: data.source
|
||||
})),
|
||||
userId,
|
||||
model: global.vectorModels[0].model,
|
||||
mode: TrainingModeEnum.index
|
||||
});
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { openaiAccountError } from '../errorCode';
|
||||
import { insertKbItem } from '@/service/pg';
|
||||
import { openaiEmbedding } from '@/pages/api/openapi/plugin/openaiEmbedding';
|
||||
import { getVector } from '@/pages/api/openapi/plugin/vector';
|
||||
import { TrainingData } from '../models/trainingData';
|
||||
import { ERROR_ENUM } from '../errorCode';
|
||||
import { TrainingModeEnum } from '@/constants/plugin';
|
||||
@@ -33,7 +33,8 @@ export async function generateVector(): Promise<any> {
|
||||
kbId: 1,
|
||||
q: 1,
|
||||
a: 1,
|
||||
source: 1
|
||||
source: 1,
|
||||
model: 1
|
||||
});
|
||||
|
||||
// task preemption
|
||||
@@ -55,10 +56,10 @@ export async function generateVector(): Promise<any> {
|
||||
];
|
||||
|
||||
// 生成词向量
|
||||
const vectors = await openaiEmbedding({
|
||||
const vectors = await getVector({
|
||||
model: data.model,
|
||||
input: dataItems.map((item) => item.q),
|
||||
userId,
|
||||
mustPay: true
|
||||
userId
|
||||
});
|
||||
|
||||
// 生成结果插入到 pg
|
||||
|
||||
@@ -1,66 +1,85 @@
|
||||
import { connectToDatabase, Bill, User, ShareChat } from '../mongo';
|
||||
import {
|
||||
ChatModelMap,
|
||||
OpenAiChatEnum,
|
||||
ChatModelType,
|
||||
embeddingModel,
|
||||
embeddingPrice
|
||||
} from '@/constants/model';
|
||||
import { BillTypeEnum } from '@/constants/user';
|
||||
import { BillSourceEnum } from '@/constants/user';
|
||||
import { getModel } from '../utils/data';
|
||||
import type { BillListItemType } from '@/types/mongoSchema';
|
||||
|
||||
export const pushChatBill = async ({
|
||||
isPay,
|
||||
chatModel,
|
||||
userId,
|
||||
export const createTaskBill = async ({
|
||||
appName,
|
||||
appId,
|
||||
textLen,
|
||||
tokens,
|
||||
type
|
||||
userId,
|
||||
source
|
||||
}: {
|
||||
isPay: boolean;
|
||||
chatModel: ChatModelType;
|
||||
userId: string;
|
||||
appName: string;
|
||||
appId: string;
|
||||
textLen: number;
|
||||
tokens: number;
|
||||
type: BillTypeEnum.chat | BillTypeEnum.openapiChat;
|
||||
userId: string;
|
||||
source: `${BillSourceEnum}`;
|
||||
}) => {
|
||||
console.log(`chat generate success. text len: ${textLen}. token len: ${tokens}. pay:${isPay}`);
|
||||
if (!isPay) return;
|
||||
const res = await Bill.create({
|
||||
userId,
|
||||
appName,
|
||||
appId,
|
||||
total: 0,
|
||||
source,
|
||||
list: []
|
||||
});
|
||||
return String(res._id);
|
||||
};
|
||||
|
||||
let billId = '';
|
||||
export const pushTaskBillListItem = async ({
|
||||
billId,
|
||||
moduleName,
|
||||
amount,
|
||||
model,
|
||||
tokenLen
|
||||
}: { billId?: string } & BillListItemType) => {
|
||||
if (!billId) return;
|
||||
try {
|
||||
await Bill.findByIdAndUpdate(billId, {
|
||||
$push: {
|
||||
list: {
|
||||
moduleName,
|
||||
amount,
|
||||
model,
|
||||
tokenLen
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (error) {}
|
||||
};
|
||||
export const finishTaskBill = async ({ billId }: { billId: string }) => {
|
||||
try {
|
||||
// update bill
|
||||
const res = await Bill.findByIdAndUpdate(billId, [
|
||||
{
|
||||
$set: {
|
||||
total: {
|
||||
$sum: '$list.amount'
|
||||
},
|
||||
time: new Date()
|
||||
}
|
||||
}
|
||||
]);
|
||||
if (!res) return;
|
||||
const total = res.list.reduce((sum, item) => sum + item.amount, 0) || 0;
|
||||
|
||||
console.log('finish bill:', total);
|
||||
|
||||
// 账号扣费
|
||||
await User.findByIdAndUpdate(res.userId, {
|
||||
$inc: { balance: -total }
|
||||
});
|
||||
} catch (error) {
|
||||
console.log('Finish bill failed:', error);
|
||||
billId && Bill.findByIdAndDelete(billId);
|
||||
}
|
||||
};
|
||||
|
||||
export const delTaskBill = async (billId?: string) => {
|
||||
if (!billId) return;
|
||||
|
||||
try {
|
||||
await connectToDatabase();
|
||||
|
||||
// 计算价格
|
||||
const unitPrice = ChatModelMap[chatModel]?.price || 3;
|
||||
const price = unitPrice * tokens;
|
||||
|
||||
try {
|
||||
// 插入 Bill 记录
|
||||
const res = await Bill.create({
|
||||
userId,
|
||||
type,
|
||||
modelName: chatModel,
|
||||
appId,
|
||||
textLen,
|
||||
tokenLen: tokens,
|
||||
price
|
||||
});
|
||||
billId = res._id;
|
||||
|
||||
// 账号扣费
|
||||
await User.findByIdAndUpdate(userId, {
|
||||
$inc: { balance: -price }
|
||||
});
|
||||
} catch (error) {
|
||||
console.log('创建账单失败:', error);
|
||||
billId && Bill.findByIdAndDelete(billId);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
await Bill.findByIdAndRemove(billId);
|
||||
} catch (error) {}
|
||||
};
|
||||
|
||||
export const updateShareChatBill = async ({
|
||||
@@ -81,22 +100,17 @@ export const updateShareChatBill = async ({
|
||||
};
|
||||
|
||||
export const pushSplitDataBill = async ({
|
||||
isPay,
|
||||
userId,
|
||||
totalTokens,
|
||||
textLen,
|
||||
type
|
||||
model,
|
||||
appName
|
||||
}: {
|
||||
isPay: boolean;
|
||||
model: string;
|
||||
userId: string;
|
||||
totalTokens: number;
|
||||
textLen: number;
|
||||
type: BillTypeEnum.QA;
|
||||
appName: string;
|
||||
}) => {
|
||||
console.log(
|
||||
`splitData generate success. text len: ${textLen}. token len: ${totalTokens}. pay:${isPay}`
|
||||
);
|
||||
if (!isPay) return;
|
||||
console.log(`splitData generate success. token len: ${totalTokens}.`);
|
||||
|
||||
let billId;
|
||||
|
||||
@@ -104,24 +118,22 @@ export const pushSplitDataBill = async ({
|
||||
await connectToDatabase();
|
||||
|
||||
// 获取模型单价格, 都是用 gpt35 拆分
|
||||
const unitPrice = ChatModelMap[OpenAiChatEnum.GPT3516k].price || 3;
|
||||
const unitPrice = global.chatModels.find((item) => item.model === model)?.price || 3;
|
||||
// 计算价格
|
||||
const price = unitPrice * totalTokens;
|
||||
const total = unitPrice * totalTokens;
|
||||
|
||||
// 插入 Bill 记录
|
||||
const res = await Bill.create({
|
||||
userId,
|
||||
type,
|
||||
modelName: OpenAiChatEnum.GPT3516k,
|
||||
textLen,
|
||||
appName,
|
||||
tokenLen: totalTokens,
|
||||
price
|
||||
total
|
||||
});
|
||||
billId = res._id;
|
||||
|
||||
// 账号扣费
|
||||
await User.findByIdAndUpdate(userId, {
|
||||
$inc: { balance: -price }
|
||||
$inc: { balance: -total }
|
||||
});
|
||||
} catch (error) {
|
||||
console.log('创建账单失败:', error);
|
||||
@@ -130,21 +142,14 @@ export const pushSplitDataBill = async ({
|
||||
};
|
||||
|
||||
export const pushGenerateVectorBill = async ({
|
||||
isPay,
|
||||
userId,
|
||||
text,
|
||||
tokenLen
|
||||
tokenLen,
|
||||
model
|
||||
}: {
|
||||
isPay: boolean;
|
||||
userId: string;
|
||||
text: string;
|
||||
tokenLen: number;
|
||||
model: string;
|
||||
}) => {
|
||||
// console.log(
|
||||
// `vector generate success. text len: ${text.length}. token len: ${tokenLen}. pay:${isPay}`
|
||||
// );
|
||||
if (!isPay) return;
|
||||
|
||||
let billId;
|
||||
|
||||
try {
|
||||
@@ -152,23 +157,22 @@ export const pushGenerateVectorBill = async ({
|
||||
|
||||
try {
|
||||
// 计算价格. 至少为1
|
||||
let price = embeddingPrice * tokenLen;
|
||||
price = price > 1 ? price : 1;
|
||||
const unitPrice = global.vectorModels.find((item) => item.model === model)?.price || 0.2;
|
||||
let total = unitPrice * tokenLen;
|
||||
total = total > 1 ? total : 1;
|
||||
|
||||
// 插入 Bill 记录
|
||||
const res = await Bill.create({
|
||||
userId,
|
||||
type: BillTypeEnum.vector,
|
||||
modelName: embeddingModel,
|
||||
textLen: text.length,
|
||||
tokenLen,
|
||||
price
|
||||
model,
|
||||
appName: '索引生成',
|
||||
total
|
||||
});
|
||||
billId = res._id;
|
||||
|
||||
// 账号扣费
|
||||
await User.findByIdAndUpdate(userId, {
|
||||
$inc: { balance: -price }
|
||||
$inc: { balance: -total }
|
||||
});
|
||||
} catch (error) {
|
||||
console.log('创建账单失败:', error);
|
||||
@@ -178,3 +182,9 @@ export const pushGenerateVectorBill = async ({
|
||||
console.log(error);
|
||||
}
|
||||
};
|
||||
|
||||
export const countModelPrice = ({ model, tokens }: { model: string; tokens: number }) => {
|
||||
const modelData = getModel(model);
|
||||
if (!modelData) return 0;
|
||||
return modelData.price * tokens;
|
||||
};
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { Schema, model, models, Model } from 'mongoose';
|
||||
import { AppSchema as AppType } from '@/types/mongoSchema';
|
||||
import { ChatModelMap, OpenAiChatEnum } from '@/constants/model';
|
||||
|
||||
const AppSchema = new Schema({
|
||||
userId: {
|
||||
@@ -24,50 +23,6 @@ const AppSchema = new Schema({
|
||||
type: Date,
|
||||
default: () => new Date()
|
||||
},
|
||||
chat: {
|
||||
relatedKbs: {
|
||||
type: [Schema.Types.ObjectId],
|
||||
ref: 'kb',
|
||||
default: []
|
||||
},
|
||||
searchSimilarity: {
|
||||
type: Number,
|
||||
default: 0.8
|
||||
},
|
||||
searchLimit: {
|
||||
type: Number,
|
||||
default: 5
|
||||
},
|
||||
searchEmptyText: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
systemPrompt: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
limitPrompt: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
maxToken: {
|
||||
type: Number,
|
||||
default: 4000,
|
||||
min: 100
|
||||
},
|
||||
temperature: {
|
||||
type: Number,
|
||||
min: 0,
|
||||
max: 10,
|
||||
default: 0
|
||||
},
|
||||
chatModel: {
|
||||
// 聊天时使用的模型
|
||||
type: String,
|
||||
enum: Object.keys(ChatModelMap),
|
||||
default: OpenAiChatEnum.GPT3516k
|
||||
}
|
||||
},
|
||||
share: {
|
||||
topNum: {
|
||||
type: Number,
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { Schema, model, models, Model } from 'mongoose';
|
||||
import { ChatModelMap, embeddingModel } from '@/constants/model';
|
||||
import { BillSchema as BillType } from '@/types/mongoSchema';
|
||||
import { BillTypeMap } from '@/constants/user';
|
||||
import { BillSourceEnum, BillSourceMap } from '@/constants/user';
|
||||
|
||||
const BillSchema = new Schema({
|
||||
userId: {
|
||||
@@ -9,36 +8,48 @@ const BillSchema = new Schema({
|
||||
ref: 'user',
|
||||
required: true
|
||||
},
|
||||
type: {
|
||||
appName: {
|
||||
type: String,
|
||||
enum: Object.keys(BillTypeMap),
|
||||
required: true
|
||||
},
|
||||
modelName: {
|
||||
type: String,
|
||||
enum: [...Object.keys(ChatModelMap), embeddingModel]
|
||||
default: ''
|
||||
},
|
||||
appId: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'app'
|
||||
ref: 'app',
|
||||
required: false
|
||||
},
|
||||
time: {
|
||||
type: Date,
|
||||
default: () => new Date()
|
||||
},
|
||||
textLen: {
|
||||
// 提示词+响应的总字数
|
||||
total: {
|
||||
type: Number,
|
||||
required: true
|
||||
},
|
||||
tokenLen: {
|
||||
// 折算成 token 的数量
|
||||
type: Number,
|
||||
required: true
|
||||
source: {
|
||||
type: String,
|
||||
enum: Object.keys(BillSourceMap),
|
||||
default: BillSourceEnum.fastgpt
|
||||
},
|
||||
price: {
|
||||
type: Number,
|
||||
required: true
|
||||
list: {
|
||||
type: [
|
||||
{
|
||||
moduleName: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
amount: {
|
||||
type: Number,
|
||||
required: true
|
||||
},
|
||||
model: {
|
||||
type: String
|
||||
},
|
||||
tokenLen: {
|
||||
type: Number
|
||||
}
|
||||
}
|
||||
],
|
||||
default: []
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
import { Schema, model, models } from 'mongoose';
|
||||
|
||||
const SystemSchema = new Schema({
|
||||
vectorMaxProcess: {
|
||||
type: Number,
|
||||
default: 10
|
||||
},
|
||||
qaMaxProcess: {
|
||||
type: Number,
|
||||
default: 10
|
||||
},
|
||||
pgIvfflatProbe: {
|
||||
type: Number,
|
||||
default: 10
|
||||
},
|
||||
sensitiveCheck: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
});
|
||||
|
||||
export const System = models['system'] || model('system', SystemSchema);
|
||||
@@ -28,13 +28,16 @@ const TrainingDataSchema = new Schema({
|
||||
enum: Object.keys(TrainingTypeMap),
|
||||
required: true
|
||||
},
|
||||
model: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
prompt: {
|
||||
// 拆分时的提示词
|
||||
// qa split prompt
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
q: {
|
||||
// 如果是
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
|
||||
@@ -2,6 +2,7 @@ import mongoose from 'mongoose';
|
||||
import tunnel from 'tunnel';
|
||||
import { startQueue } from './utils/tools';
|
||||
import { updateSystemEnv } from '@/pages/api/system/updateEnv';
|
||||
import { initSystemModels } from '@/pages/api/system/getInitData';
|
||||
|
||||
/**
|
||||
* 连接 MongoDB 数据库
|
||||
@@ -10,6 +11,7 @@ export async function connectToDatabase(): Promise<void> {
|
||||
if (global.mongodb) {
|
||||
return;
|
||||
}
|
||||
global.mongodb = 'connecting';
|
||||
|
||||
// init global data
|
||||
global.qaQueueLen = 0;
|
||||
@@ -31,8 +33,9 @@ export async function connectToDatabase(): Promise<void> {
|
||||
}
|
||||
});
|
||||
}
|
||||
initSystemModels();
|
||||
updateSystemEnv();
|
||||
|
||||
global.mongodb = 'connecting';
|
||||
try {
|
||||
mongoose.set('strictQuery', true);
|
||||
global.mongodb = await mongoose.connect(process.env.MONGODB_URI as string, {
|
||||
@@ -49,7 +52,6 @@ export async function connectToDatabase(): Promise<void> {
|
||||
}
|
||||
|
||||
// init function
|
||||
updateSystemEnv();
|
||||
startQueue();
|
||||
}
|
||||
|
||||
@@ -66,5 +68,4 @@ export * from './models/collection';
|
||||
export * from './models/shareChat';
|
||||
export * from './models/kb';
|
||||
export * from './models/inform';
|
||||
export * from './models/system';
|
||||
export * from './models/image';
|
||||
|
||||
@@ -92,7 +92,7 @@ export const sseErrRes = (res: NextApiResponse, error: any) => {
|
||||
} else if (openaiError[error?.response?.statusText]) {
|
||||
msg = openaiError[error.response.statusText];
|
||||
}
|
||||
console.log('sse error', error);
|
||||
console.log('sse error => ', error);
|
||||
|
||||
sseResponse({
|
||||
res,
|
||||
|
||||
@@ -1,15 +1,11 @@
|
||||
import type { NextApiRequest } from 'next';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import Cookie from 'cookie';
|
||||
import { Chat, App, OpenApi, User, ShareChat, KB } from '../mongo';
|
||||
import { App, OpenApi, User, ShareChat, KB } from '../mongo';
|
||||
import type { AppSchema } from '@/types/mongoSchema';
|
||||
import type { ChatItemType } from '@/types/chat';
|
||||
import mongoose from 'mongoose';
|
||||
import { defaultApp } from '@/constants/model';
|
||||
import { formatPrice } from '@/utils/user';
|
||||
import { ERROR_ENUM } from '../errorCode';
|
||||
import { ChatModelType, OpenAiChatEnum } from '@/constants/model';
|
||||
import { hashPassword } from '@/service/utils/tools';
|
||||
|
||||
export type AuthType = 'token' | 'root' | 'apikey';
|
||||
|
||||
@@ -35,6 +31,19 @@ export const parseCookie = (cookie?: string): Promise<string> => {
|
||||
});
|
||||
};
|
||||
|
||||
/* auth balance */
|
||||
export const authBalanceByUid = async (uid: string) => {
|
||||
const user = await User.findById(uid);
|
||||
if (!user) {
|
||||
return Promise.reject(ERROR_ENUM.unAuthorization);
|
||||
}
|
||||
|
||||
if (!user.openaiKey && formatPrice(user.balance) <= 0) {
|
||||
return Promise.reject(ERROR_ENUM.insufficientQuota);
|
||||
}
|
||||
return user;
|
||||
};
|
||||
|
||||
/* uniform auth user */
|
||||
export const authUser = async ({
|
||||
req,
|
||||
@@ -144,14 +153,7 @@ export const authUser = async ({
|
||||
|
||||
// balance check
|
||||
if (authBalance) {
|
||||
const user = await User.findById(uid);
|
||||
if (!user) {
|
||||
return Promise.reject(ERROR_ENUM.unAuthorization);
|
||||
}
|
||||
|
||||
if (!user.openaiKey && formatPrice(user.balance) <= 0) {
|
||||
return Promise.reject(ERROR_ENUM.insufficientQuota);
|
||||
}
|
||||
await authBalanceByUid(uid);
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -166,43 +168,6 @@ export const getSystemOpenAiKey = () => {
|
||||
return process.env.ONEAPI_KEY || process.env.OPENAIKEY || '';
|
||||
};
|
||||
|
||||
/* 获取 api 请求的 key */
|
||||
export const getApiKey = async ({
|
||||
model,
|
||||
userId,
|
||||
mustPay = false
|
||||
}: {
|
||||
model: ChatModelType;
|
||||
userId: string;
|
||||
mustPay?: boolean;
|
||||
}) => {
|
||||
const user = await User.findById(userId, 'openaiKey balance');
|
||||
if (!user) {
|
||||
return Promise.reject(ERROR_ENUM.unAuthorization);
|
||||
}
|
||||
|
||||
const userOpenAiKey = user.openaiKey || '';
|
||||
const systemAuthKey = getSystemOpenAiKey();
|
||||
|
||||
// 有自己的key
|
||||
if (!mustPay && userOpenAiKey) {
|
||||
return {
|
||||
userOpenAiKey,
|
||||
systemAuthKey: ''
|
||||
};
|
||||
}
|
||||
|
||||
// 平台账号余额校验
|
||||
if (formatPrice(user.balance) <= 0) {
|
||||
return Promise.reject(ERROR_ENUM.insufficientQuota);
|
||||
}
|
||||
|
||||
return {
|
||||
userOpenAiKey: '',
|
||||
systemAuthKey
|
||||
};
|
||||
};
|
||||
|
||||
// 模型使用权校验
|
||||
export const authApp = async ({
|
||||
appId,
|
||||
@@ -232,14 +197,6 @@ export const authApp = async ({
|
||||
if (userId !== String(app.userId)) return Promise.reject(ERROR_ENUM.unAuthModel);
|
||||
}
|
||||
|
||||
// do not share detail info
|
||||
if (!reserveDetail && !app.share.isShareDetail && userId !== String(app.userId)) {
|
||||
app.chat = {
|
||||
...defaultApp.chat,
|
||||
chatModel: app.chat.chatModel
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
app,
|
||||
showModelDetail: userId === String(app.userId)
|
||||
|
||||
@@ -1,13 +1,8 @@
|
||||
import { ChatItemType } from '@/types/chat';
|
||||
import { modelToolMap } from '@/utils/plugin';
|
||||
import type { ChatModelType } from '@/constants/model';
|
||||
import { ChatRoleEnum, sseResponseEventEnum } from '@/constants/chat';
|
||||
import { sseResponse } from '../tools';
|
||||
import { ChatRoleEnum } from '@/constants/chat';
|
||||
import { OpenAiChatEnum } from '@/constants/model';
|
||||
import { chatResponse, openAiStreamResponse } from './openai';
|
||||
import type { NextApiResponse } from 'next';
|
||||
import { textAdaptGptResponse } from '@/utils/adapt';
|
||||
import { parseStreamChunk } from '@/utils/adapt';
|
||||
|
||||
export type ChatCompletionType = {
|
||||
apiKey: string;
|
||||
@@ -36,11 +31,6 @@ export type StreamResponseReturnType = {
|
||||
finishMessages: ChatItemType[];
|
||||
};
|
||||
|
||||
export const modelServiceToolMap = {
|
||||
chatCompletion: chatResponse,
|
||||
streamResponse: openAiStreamResponse
|
||||
};
|
||||
|
||||
/* delete invalid symbol */
|
||||
const simplifyStr = (str = '') =>
|
||||
str
|
||||
@@ -54,7 +44,7 @@ export const ChatContextFilter = ({
|
||||
prompts,
|
||||
maxTokens
|
||||
}: {
|
||||
model: ChatModelType;
|
||||
model: string;
|
||||
prompts: ChatItemType[];
|
||||
maxTokens: number;
|
||||
}) => {
|
||||
@@ -111,126 +101,3 @@ export const ChatContextFilter = ({
|
||||
|
||||
return [...systemPrompts, ...chats];
|
||||
};
|
||||
|
||||
/* stream response */
|
||||
export const resStreamResponse = async ({
|
||||
model,
|
||||
res,
|
||||
chatResponse,
|
||||
prompts
|
||||
}: StreamResponseType & {
|
||||
model: ChatModelType;
|
||||
}) => {
|
||||
// 创建响应流
|
||||
res.setHeader('Content-Type', 'text/event-stream;charset=utf-8');
|
||||
res.setHeader('Access-Control-Allow-Origin', '*');
|
||||
res.setHeader('X-Accel-Buffering', 'no');
|
||||
res.setHeader('Cache-Control', 'no-cache, no-transform');
|
||||
|
||||
const { responseContent, totalTokens, finishMessages } = await modelServiceToolMap.streamResponse(
|
||||
{
|
||||
chatResponse,
|
||||
prompts,
|
||||
res,
|
||||
model
|
||||
}
|
||||
);
|
||||
|
||||
return { responseContent, totalTokens, finishMessages };
|
||||
};
|
||||
|
||||
/* stream response */
|
||||
export const V2_StreamResponse = async ({
|
||||
model,
|
||||
res,
|
||||
chatResponse,
|
||||
prompts
|
||||
}: StreamResponseType & {
|
||||
model: ChatModelType;
|
||||
}) => {
|
||||
let responseContent = '';
|
||||
let error: any = null;
|
||||
let truncateData = '';
|
||||
const clientRes = async (data: string) => {
|
||||
//部分代理会导致流式传输时的数据被截断,不为json格式,这里做一个兼容
|
||||
const { content = '' } = (() => {
|
||||
try {
|
||||
if (truncateData) {
|
||||
try {
|
||||
//判断是否为json,如果是的话直接跳过后续拼装操作,注意极端情况下可能出现截断成3截以上情况也可以兼容
|
||||
JSON.parse(data);
|
||||
} catch (e) {
|
||||
data = truncateData + data;
|
||||
}
|
||||
truncateData = '';
|
||||
}
|
||||
const json = JSON.parse(data);
|
||||
const content: string = json?.choices?.[0].delta.content || '';
|
||||
error = json.error;
|
||||
responseContent += content;
|
||||
return { content };
|
||||
} catch (error) {
|
||||
truncateData = data;
|
||||
return {};
|
||||
}
|
||||
})();
|
||||
|
||||
if (res.closed || error) return;
|
||||
|
||||
if (data === '[DONE]') {
|
||||
sseResponse({
|
||||
res,
|
||||
event: sseResponseEventEnum.answer,
|
||||
data: textAdaptGptResponse({
|
||||
text: null,
|
||||
finish_reason: 'stop'
|
||||
})
|
||||
});
|
||||
sseResponse({
|
||||
res,
|
||||
event: sseResponseEventEnum.answer,
|
||||
data: '[DONE]'
|
||||
});
|
||||
} else {
|
||||
sseResponse({
|
||||
res,
|
||||
event: sseResponseEventEnum.answer,
|
||||
data: textAdaptGptResponse({
|
||||
text: content
|
||||
})
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
for await (const chunk of chatResponse.data as any) {
|
||||
if (res.closed) break;
|
||||
const parse = parseStreamChunk(chunk);
|
||||
parse.forEach((item) => clientRes(item.data));
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('pipe error', error);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
console.log(error);
|
||||
return Promise.reject(error);
|
||||
}
|
||||
|
||||
// count tokens
|
||||
const finishMessages = prompts.concat({
|
||||
obj: ChatRoleEnum.AI,
|
||||
value: responseContent
|
||||
});
|
||||
|
||||
const totalTokens = modelToolMap.countTokens({
|
||||
model,
|
||||
messages: finishMessages
|
||||
});
|
||||
|
||||
return {
|
||||
responseContent,
|
||||
totalTokens,
|
||||
finishMessages
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,133 +0,0 @@
|
||||
import { Configuration, OpenAIApi } from 'openai';
|
||||
import { axiosConfig } from '../tools';
|
||||
import { ChatModelMap, OpenAiChatEnum } from '@/constants/model';
|
||||
import { adaptChatItem_openAI } from '@/utils/plugin/openai';
|
||||
import { modelToolMap } from '@/utils/plugin';
|
||||
import { ChatCompletionType, ChatContextFilter, StreamResponseType } from './index';
|
||||
import { ChatRoleEnum } from '@/constants/chat';
|
||||
import { parseStreamChunk } from '@/utils/adapt';
|
||||
|
||||
export const getOpenAIApi = (apiKey: string) => {
|
||||
const openaiBaseUrl = process.env.OPENAI_BASE_URL || 'https://api.openai.com/v1';
|
||||
return new OpenAIApi(
|
||||
new Configuration({
|
||||
basePath: apiKey === process.env.ONEAPI_KEY ? process.env.ONEAPI_URL : openaiBaseUrl
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
/* 模型对话 */
|
||||
export const chatResponse = async ({
|
||||
model,
|
||||
apiKey,
|
||||
temperature,
|
||||
maxToken = 4000,
|
||||
messages,
|
||||
stream
|
||||
}: ChatCompletionType & { model: `${OpenAiChatEnum}` }) => {
|
||||
const modelTokenLimit = ChatModelMap[model]?.contextMaxToken || 4000;
|
||||
const filterMessages = ChatContextFilter({
|
||||
model,
|
||||
prompts: messages,
|
||||
maxTokens: Math.ceil(modelTokenLimit - 300) // filter token. not response maxToken
|
||||
});
|
||||
|
||||
const adaptMessages = adaptChatItem_openAI({ messages: filterMessages, reserveId: false });
|
||||
const chatAPI = getOpenAIApi(apiKey);
|
||||
|
||||
const promptsToken = modelToolMap.countTokens({
|
||||
model,
|
||||
messages: filterMessages
|
||||
});
|
||||
|
||||
maxToken = maxToken + promptsToken > modelTokenLimit ? modelTokenLimit - promptsToken : maxToken;
|
||||
|
||||
const response = await chatAPI.createChatCompletion(
|
||||
{
|
||||
model,
|
||||
temperature: Number(temperature || 0),
|
||||
max_tokens: maxToken,
|
||||
messages: adaptMessages,
|
||||
frequency_penalty: 0.5, // 越大,重复内容越少
|
||||
presence_penalty: -0.5, // 越大,越容易出现新内容
|
||||
stream
|
||||
// stop: ['.!?。']
|
||||
},
|
||||
{
|
||||
timeout: stream ? 60000 : 480000,
|
||||
responseType: stream ? 'stream' : 'json',
|
||||
...axiosConfig(apiKey)
|
||||
}
|
||||
);
|
||||
|
||||
const responseText = stream ? '' : response.data.choices?.[0].message?.content || '';
|
||||
const totalTokens = stream ? 0 : response.data.usage?.total_tokens || 0;
|
||||
|
||||
return {
|
||||
streamResponse: response,
|
||||
responseMessages: filterMessages.concat({ obj: 'AI', value: responseText }),
|
||||
responseText,
|
||||
totalTokens
|
||||
};
|
||||
};
|
||||
|
||||
/* openai stream response */
|
||||
export const openAiStreamResponse = async ({
|
||||
res,
|
||||
model,
|
||||
chatResponse,
|
||||
prompts
|
||||
}: StreamResponseType & {
|
||||
model: `${OpenAiChatEnum}`;
|
||||
}) => {
|
||||
try {
|
||||
let responseContent = '';
|
||||
|
||||
const clientRes = async (data: string) => {
|
||||
const { content = '' } = (() => {
|
||||
try {
|
||||
const json = JSON.parse(data);
|
||||
const content: string = json?.choices?.[0].delta.content || '';
|
||||
responseContent += content;
|
||||
return { content };
|
||||
} catch (error) {
|
||||
return {};
|
||||
}
|
||||
})();
|
||||
|
||||
if (data === '[DONE]') return;
|
||||
|
||||
!res.closed && content && res.write(content);
|
||||
};
|
||||
|
||||
try {
|
||||
for await (const chunk of chatResponse.data as any) {
|
||||
if (res.closed) break;
|
||||
|
||||
const parse = parseStreamChunk(chunk);
|
||||
parse.forEach((item) => clientRes(item.data));
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('pipe error', error);
|
||||
}
|
||||
|
||||
// count tokens
|
||||
const finishMessages = prompts.concat({
|
||||
obj: ChatRoleEnum.AI,
|
||||
value: responseContent
|
||||
});
|
||||
|
||||
const totalTokens = modelToolMap.countTokens({
|
||||
model,
|
||||
messages: finishMessages
|
||||
});
|
||||
|
||||
return {
|
||||
responseContent,
|
||||
totalTokens,
|
||||
finishMessages
|
||||
};
|
||||
} catch (error) {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
};
|
||||
14
client/src/service/utils/data.ts
Normal file
14
client/src/service/utils/data.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
export const getChatModel = (model: string) => {
|
||||
return global.chatModels.find((item) => item.model === model);
|
||||
};
|
||||
export const getVectorModel = (model: string) => {
|
||||
return global.vectorModels.find((item) => item.model === model);
|
||||
};
|
||||
export const getQAModel = (model: string) => {
|
||||
return global.qaModels.find((item) => item.model === model);
|
||||
};
|
||||
export const getModel = (model: string) => {
|
||||
return [...global.chatModels, ...global.vectorModels, ...global.qaModels].find(
|
||||
(item) => item.model === model
|
||||
);
|
||||
};
|
||||
@@ -4,7 +4,6 @@ import crypto from 'crypto';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import { generateQA } from '../events/generateQA';
|
||||
import { generateVector } from '../events/generateVector';
|
||||
import { sseResponseEventEnum } from '@/constants/chat';
|
||||
|
||||
/* 密码加密 */
|
||||
export const hashPassword = (psw: string) => {
|
||||
@@ -33,20 +32,6 @@ export const clearCookie = (res: NextApiResponse) => {
|
||||
res.setHeader('Set-Cookie', 'token=; Path=/; Max-Age=0');
|
||||
};
|
||||
|
||||
/* openai axios config */
|
||||
export const axiosConfig = (apikey: string) => {
|
||||
const openaiBaseUrl = process.env.OPENAI_BASE_URL || 'https://api.openai.com/v1';
|
||||
|
||||
return {
|
||||
baseURL: apikey === process.env.ONEAPI_KEY ? process.env.ONEAPI_URL : openaiBaseUrl, // 此处仅对非 npm 模块有效
|
||||
httpsAgent: global.httpsAgent,
|
||||
headers: {
|
||||
Authorization: `Bearer ${apikey}`,
|
||||
auth: process.env.OPENAI_BASE_URL_AUTH || ''
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
export function withNextCors(handler: NextApiHandler): NextApiHandler {
|
||||
return async function nextApiHandlerWrappedWithNextCors(
|
||||
req: NextApiRequest,
|
||||
|
||||
Reference in New Issue
Block a user