doc gpt V0.2

This commit is contained in:
archer
2023-02-19 14:35:25 +08:00
parent cc5cf99e7a
commit 0ecf576e4e
124 changed files with 11780 additions and 573 deletions

View File

@@ -0,0 +1,5 @@
export enum OpenAiTuneStatusEnum {
cancelled = 'cancelled',
succeeded = 'succeeded',
pending = 'pending'
}

3
src/service/errorCode.ts Normal file
View File

@@ -0,0 +1,3 @@
export const openaiError: Record<string, string> = {
context_length_exceeded: '内容超出长度'
};

View File

@@ -0,0 +1,24 @@
import { Schema, model, models } from 'mongoose';
const AuthCodeSchema = new Schema({
email: {
type: String,
required: true
},
code: {
type: String,
required: true,
length: 6
},
type: {
type: String,
enum: ['register', 'findPassword'],
required: true
},
expiredTime: {
type: Number,
default: () => Date.now() + 5 * 60 * 1000
}
});
export const AuthCode = models['auth_code'] || model('auth_code', AuthCodeSchema);

View File

@@ -0,0 +1,26 @@
import { Schema, model, models } from 'mongoose';
const ChatSchema = new Schema({
userId: {
type: Schema.Types.ObjectId,
ref: 'user',
required: true
},
modelId: {
type: Schema.Types.ObjectId,
ref: 'model',
required: true
},
expiredTime: {
// 过期时间
type: Number,
required: true
},
loadAmount: {
// 剩余加载次数
type: Number,
required: true
}
});
export const Chat = models['chat'] || model('chat', ChatSchema);

View File

@@ -0,0 +1,28 @@
import { Schema, model, models } from 'mongoose';
const ChatWindowSchema = new Schema({
chatId: {
type: Schema.Types.ObjectId,
ref: 'chat',
required: true
},
updateTime: {
type: Number,
required: true
},
content: [
{
obj: {
type: String,
required: true,
enum: ['Human', 'AI', 'SYSTEM']
},
value: {
type: String,
required: true
}
}
]
});
export const ChatWindow = models['chatWindow'] || model('chatWindow', ChatWindowSchema);

View File

@@ -0,0 +1,86 @@
import { Schema, model, models } from 'mongoose';
const ModelSchema = new Schema({
name: {
type: String,
required: true
},
avatar: {
type: String,
default: '/imgs/modelAvatar.png'
},
systemPrompt: {
type: String,
default: ''
},
userId: {
type: Schema.Types.ObjectId,
ref: 'user',
required: true
},
status: {
type: String,
required: true,
enum: ['waiting', 'running', 'training', 'closed']
},
updateTime: {
type: Date,
default: () => new Date()
},
trainingTimes: {
type: Number,
default: 0
},
service: {
company: {
type: String,
required: true,
enum: ['openai']
},
trainId: {
// 训练时需要的 ID
type: String,
required: true
},
chatModel: {
// 聊天时使用的模型
type: String,
required: true
},
modelName: {
// 底层模型的名称
type: String,
required: true
}
},
security: {
type: {
domain: {
type: [String],
default: ['*']
},
contextMaxLen: {
type: Number,
default: 20
},
contentMaxLen: {
type: Number,
default: 4000
},
expiredTime: {
type: Number,
default: 1,
set: (val: number) => val * (60 * 60 * 1000)
},
maxLoadAmount: {
// 负数代表不限制
type: Number,
default: -1
}
},
default: {},
required: true
}
});
export const Model = models['model'] || model('model', ModelSchema);

View File

@@ -0,0 +1,28 @@
import { Schema, model, models } from 'mongoose';
const TrainingSChema = new Schema({
serviceName: {
// 模型厂商名
type: String,
required: true
},
tuneId: {
// 微调进程 ID
type: String,
required: true
},
modelId: {
// 关联模型的 ID
type: Schema.Types.ObjectId,
ref: 'model',
required: true
},
status: {
// 状态值
type: String,
required: true,
enum: ['pending', 'succeed', 'errored', 'canceled']
}
});
export const Training = models['training'] || model('training', TrainingSChema);

View File

@@ -0,0 +1,40 @@
import { Schema, model, models } from 'mongoose';
import { hashPassword } from '@/service/utils/tools';
const UserSchema = new Schema({
email: {
type: String,
required: true,
unique: true // 唯一
},
password: {
type: String,
required: true,
set: (val: string) => hashPassword(val),
get: (val: string) => hashPassword(val),
select: false
},
balance: {
type: Number,
default: 0
},
accounts: [
{
type: {
type: String,
required: true,
enum: ['openai'] // 定义允许的type
},
value: {
type: String,
required: true
}
}
],
createTime: {
type: Date,
default: () => new Date()
}
});
export const User = models['user'] || model('user', UserSchema);

23
src/service/mongo.ts Normal file
View File

@@ -0,0 +1,23 @@
import mongoose from 'mongoose';
import type { Mongoose } from 'mongoose';
let cachedClient: Mongoose;
export async function connectToDatabase() {
if (cachedClient && cachedClient.connection.readyState === 1) {
return cachedClient;
}
cachedClient = await mongoose.connect(process.env.MONGODB_UR as string, {
dbName: 'doc_gpt'
});
return cachedClient;
}
export * from './models/authCode';
export * from './models/chat';
export * from './models/model';
export * from './models/user';
export * from './models/training';
export * from './models/chatWindow';

View File

@@ -0,0 +1,21 @@
import { ChatItemType } from '../types/chat';
export const chatWindows = new Map<string, ChatItemType[]>();
/**
* 获取聊天窗口信息
*/
export const getWindowMessages = (id: string) => {
return chatWindows.get(id) || [];
};
export const pushWindowMessage = (id: string, prompt: ChatItemType) => {
const messages = chatWindows.get(id) || [];
messages.push(prompt);
chatWindows.set(id, messages);
return messages;
};
export const deleteWindow = (id: string) => {
chatWindows.delete(id);
};

36
src/service/response.ts Normal file
View File

@@ -0,0 +1,36 @@
import { NextApiResponse } from 'next';
import { openaiError } from './errorCode';
export interface ResponseType<T = any> {
code: number;
message: string;
data: T;
}
export const jsonRes = (
res: NextApiResponse,
props?: {
code?: number;
message?: string;
data?: any;
error?: any;
}
) => {
const { code = 200, message = '', data = null, error } = props || {};
let msg = message;
if ((code < 200 || code >= 400) && !message) {
msg =
typeof error === 'string'
? error
: openaiError[error?.response?.data?.message] || error?.message || '请求错误';
console.log(msg);
}
res.json({
code,
message: msg,
data
});
};

45
src/service/utils/chat.ts Normal file
View File

@@ -0,0 +1,45 @@
import { Configuration, OpenAIApi } from 'openai';
import { Chat } from '../mongo';
export const getOpenAIApi = (apiKey: string) => {
const configuration = new Configuration({
apiKey
});
return new OpenAIApi(configuration, undefined);
};
export const authChat = async (chatId: string) => {
// 获取 chat 数据
const chat = await Chat.findById(chatId)
.populate({
path: 'modelId',
options: {
strictPopulate: false
}
})
.populate({
path: 'userId',
options: {
strictPopulate: false
}
});
if (!chat || !chat.modelId || !chat.userId) {
return Promise.reject('聊天已过期');
}
// 获取 user 的 apiKey
const user = chat.userId;
const userApiKey = user.accounts?.find((item: any) => item.type === 'openai')?.value;
if (!userApiKey) {
return Promise.reject('该用户缺少ApiKey, 无法请求');
}
return {
userApiKey,
chat
};
};

View File

@@ -0,0 +1,63 @@
import * as nodemailer from 'nodemailer';
import { EmailTypeEnum } from '@/constants/common';
import dayjs from 'dayjs';
const myEmail = process.env.MY_MAIL;
let mailTransport = nodemailer.createTransport({
// host: 'smtp.qq.email',
service: 'qq',
secure: true, //安全方式发送,建议都加上
auth: {
user: myEmail,
pass: process.env.MAILE_CODE
}
});
const emailMap: { [key: string]: any } = {
[EmailTypeEnum.register]: {
subject: '注册 DocGPT 账号',
html: (code: string) => `<div>您正在注册 DocGPT 账号,验证码为:${code}</div>`
},
[EmailTypeEnum.findPassword]: {
subject: '修改 DocGPT 密码',
html: (code: string) => `<div>您正在修改 DocGPT 账号密码,验证码为:${code}</div>`
}
};
export const sendCode = (email: string, code: string, type: `${EmailTypeEnum}`) => {
return new Promise((resolve, reject) => {
const options = {
from: `"DocGPT" ${myEmail}`,
to: email,
subject: emailMap[type]?.subject,
html: emailMap[type]?.html(code)
};
mailTransport.sendMail(options, function (err, msg) {
if (err) {
console.log(err);
reject('邮箱异常');
} else {
resolve('');
}
});
});
};
export const sendTrainSucceed = (email: string, modelName: string) => {
return new Promise((resolve, reject) => {
const options = {
from: `"DocGPT" ${myEmail}`,
to: email,
subject: '模型训练完成通知',
html: `你的模型 ${modelName} 已于 ${dayjs().format('YYYY-MM-DD HH:mm')} 训练完成!`
};
mailTransport.sendMail(options, function (err, msg) {
if (err) {
console.log(err);
reject('邮箱异常');
} else {
resolve('');
}
});
});
};

View File

@@ -0,0 +1,63 @@
import crypto from 'crypto';
import jwt from 'jsonwebtoken';
import { User } from '../models/user';
import tunnel from 'tunnel';
/* 密码加密 */
export const hashPassword = (psw: string) => {
return crypto.createHash('sha256').update(psw).digest('hex');
};
/* 生成 token */
export const generateToken = (userId: string) => {
const key = process.env.TOKEN_KEY as string;
const token = jwt.sign(
{
userId,
exp: Math.floor(Date.now() / 1000) + 60 * 60 * 24 * 7
},
key
);
return token;
};
/* 校验 token */
export const authToken = (token: string): Promise<string> => {
return new Promise((resolve, reject) => {
const key = process.env.TOKEN_KEY as string;
jwt.verify(token, key, function (err, decoded: any) {
if (err || !decoded?.userId) {
reject('凭证无效');
return;
}
resolve(decoded.userId);
});
});
};
/* 获取用户的 openai APIkey */
export const getUserOpenaiKey = async (userId: string) => {
const user = await User.findById(userId);
const userApiKey = user?.accounts?.find((item: any) => item.type === 'openai')?.value;
if (!userApiKey) {
return Promise.reject('缺少ApiKey, 无法请求');
}
return Promise.resolve(userApiKey);
};
/* 代理 */
export const openaiProxy: any =
process.env.AXIOS_PROXY_PORT && process.env.AXIOS_PROXY_HOST
? {
httpsAgent: tunnel.httpsOverHttp({
proxy: {
host: process.env.AXIOS_PROXY_HOST,
port: +process.env.AXIOS_PROXY_PORT
}
}),
proxy: false
}
: undefined;