This commit is contained in:
Archer
2023-10-22 23:54:04 +08:00
committed by GitHub
parent 3091a90df6
commit a3534407bf
365 changed files with 7266 additions and 6055 deletions

View File

@@ -0,0 +1,120 @@
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.rootkey = process.env.ROOT_KEY;
}
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 && (data.code < 200 || data.code >= 400)) {
return Promise.reject(data);
}
return data.data;
}
/**
* 响应错误
*/
function responseError(err: any) {
if (!err) {
return Promise.reject({ message: '未知错误' });
}
if (typeof err === 'string') {
return Promise.reject({ message: err });
}
if (err?.response?.data) {
return Promise.reject(err?.response?.data);
}
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));
export function request(url: string, data: any, config: ConfigType, method: Method): any {
if (!global.systemEnv?.pluginBaseUrl) {
return Promise.reject('商业版插件加载中...');
}
/* 去空 */
for (const key in data) {
if (data[key] === null || data[key] === undefined) {
delete data[key];
}
}
return instance
.request({
baseURL: global.systemEnv.pluginBaseUrl,
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');
}

View File

@@ -0,0 +1,19 @@
import type { NextApiResponse, NextApiHandler, NextApiRequest } from 'next';
import NextCors from 'nextjs-cors';
export function withNextCors(handler: NextApiHandler): NextApiHandler {
return async function nextApiHandlerWrappedWithNextCors(
req: NextApiRequest,
res: NextApiResponse
) {
const methods = ['GET', 'eHEAD', 'PUT', 'PATCH', 'POST', 'DELETE'];
const origin = req.headers.origin;
await NextCors(req, res, {
methods,
origin: origin,
optionsSuccessStatus: 200
});
return handler(req, res);
};
}

View File

@@ -0,0 +1,13 @@
import tunnel from 'tunnel';
export function initHttpAgent() {
// proxy obj
if (process.env.AXIOS_PROXY_HOST && process.env.AXIOS_PROXY_PORT) {
global.httpsAgent = tunnel.httpsOverHttp({
proxy: {
host: process.env.AXIOS_PROXY_HOST,
port: +process.env.AXIOS_PROXY_PORT
}
});
}
}

View File

@@ -0,0 +1,6 @@
import mongoose from 'mongoose';
export default mongoose;
export * from 'mongoose';
export const connectionMongo = global.mongodb || mongoose;

View File

@@ -0,0 +1,76 @@
import mongoose from './index';
import 'winston-mongodb';
import { createLogger, format, transports } from 'winston';
/**
* connect MongoDB and init data
*/
export async function connectMongo({
beforeHook,
afterHook
}: {
beforeHook?: () => any;
afterHook?: () => any;
}): Promise<void> {
if (global.mongodb) {
return;
}
global.mongodb = mongoose;
beforeHook && (await beforeHook());
// logger
initLogger();
console.log('mongo start connect');
try {
mongoose.set('strictQuery', true);
await mongoose.connect(process.env.MONGODB_URI as string, {
bufferCommands: true,
maxConnecting: Number(process.env.DB_MAX_LINK || 5),
maxPoolSize: Number(process.env.DB_MAX_LINK || 5),
minPoolSize: 2,
connectTimeoutMS: 20000,
waitQueueTimeoutMS: 20000
});
console.log('mongo connected');
afterHook && (await afterHook());
} catch (error) {
console.log('error->', 'mongo connect error', error);
global.mongodb = undefined;
}
}
function initLogger() {
global.logger = createLogger({
transports: [
new transports.MongoDB({
db: process.env.MONGODB_URI as string,
collection: 'server_logs',
options: {
useUnifiedTopology: true
},
cappedSize: 500000000,
tryReconnect: true,
metaKey: 'meta',
format: format.combine(format.timestamp(), format.json())
}),
new transports.Console({
format: format.combine(
format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
format.printf((info) => {
if (info.level === 'error') {
console.log(info.meta);
return `[${info.level.toLocaleUpperCase()}]: ${[info.timestamp]}: ${info.message}`;
}
return `[${info.level.toLocaleUpperCase()}]: ${[info.timestamp]}: ${info.message}${
info.meta ? `: ${JSON.stringify(info.meta)}` : ''
}`;
})
)
})
]
});
}

View File

@@ -0,0 +1,39 @@
import mongoose from './index';
export class MongoSession {
tasks: (() => Promise<any>)[] = [];
session: mongoose.mongo.ClientSession | null = null;
opts: {
session: mongoose.mongo.ClientSession;
new: boolean;
} | null = null;
constructor() {}
async init() {
this.session = await mongoose.startSession();
this.opts = { session: this.session, new: true };
}
push(
tasks: ((opts: {
session: mongoose.mongo.ClientSession;
new: boolean;
}) => () => Promise<any>)[] = []
) {
if (!this.opts) return;
// this.tasks = this.tasks.concat(tasks.map((item) => item(this.opts)));
}
async run() {
if (!this.session || !this.opts) return;
try {
this.session.startTransaction();
const opts = { session: this.session, new: true };
await this.session.commitTransaction();
} catch (error) {
await this.session.abortTransaction();
console.error(error);
}
this.session.endSession();
}
}

View File

@@ -0,0 +1,7 @@
import type { Mongoose } from 'mongoose';
import type { Logger } from 'winston';
declare global {
var mongodb: Mongoose | undefined;
var logger: Logger;
}

View File

@@ -0,0 +1,39 @@
import type { NextApiResponse } from 'next';
export function responseWriteController({
res,
readStream
}: {
res: NextApiResponse;
readStream: any;
}) {
res.on('drain', () => {
readStream.resume();
});
return (text: string | Buffer) => {
const writeResult = res.write(text);
if (!writeResult) {
readStream?.pause();
}
};
}
export function responseWrite({
res,
write,
event,
data
}: {
res?: NextApiResponse;
write?: (text: string) => void;
event?: string;
data: string;
}) {
const Write = write || res?.write;
if (!Write) return;
event && Write(`event: ${event}\n`);
Write(`data: ${data}\n\n`);
}

View File

@@ -0,0 +1,17 @@
import type { UserModelSchema } from '@fastgpt/global/support/user/type';
import OpenAI from 'openai';
export const openaiBaseUrl = process.env.OPENAI_BASE_URL || 'https://api.openai.com/v1';
export const baseUrl = process.env.ONEAPI_URL || openaiBaseUrl;
export const systemAIChatKey = process.env.CHAT_API_KEY || '';
export const getAIApi = (props?: UserModelSchema['openaiAccount'], timeout = 6000) => {
return new OpenAI({
apiKey: props?.key || systemAIChatKey,
baseURL: props?.baseUrl || baseUrl,
httpAgent: global.httpsAgent,
timeout,
maxRetries: 2
});
};

View File

@@ -0,0 +1,57 @@
import type { ChatCompletionRequestMessage } from '@fastgpt/global/core/ai/type.d';
import { getAIApi } from '../config';
export const Prompt_QuestionGuide = `我不太清楚问你什么问题,请帮我生成 3 个问题引导我继续提问。问题的长度应小于20个字符按 JSON 格式返回: ["问题1", "问题2", "问题3"]`;
export async function createQuestionGuide({
messages,
model
}: {
messages: ChatCompletionRequestMessage[];
model: string;
}) {
const ai = getAIApi(undefined, 48000);
const data = await ai.chat.completions.create({
model: model,
temperature: 0,
max_tokens: 200,
messages: [
...messages,
{
role: 'user',
content: Prompt_QuestionGuide
}
],
stream: false
});
const answer = data.choices?.[0]?.message?.content || '';
const totalTokens = data.usage?.total_tokens || 0;
const start = answer.indexOf('[');
const end = answer.lastIndexOf(']');
if (start === -1 || end === -1) {
return {
result: [],
tokens: totalTokens
};
}
const jsonStr = answer
.substring(start, end + 1)
.replace(/(\\n|\\)/g, '')
.replace(/ /g, '');
try {
return {
result: JSON.parse(jsonStr),
tokens: totalTokens
};
} catch (error) {
return {
result: [],
tokens: totalTokens
};
}
}

View File

@@ -0,0 +1,26 @@
import { ERROR_ENUM } from '@fastgpt/global/common/error/errorCode';
import { MongoDatasetCollection } from './collection/schema';
import { DatasetSchemaType } from '@fastgpt/global/core/dataset/type';
export async function authCollection({
collectionId,
userId
}: {
collectionId: string;
userId: string;
}) {
const collection = await MongoDatasetCollection.findOne({
_id: collectionId,
userId
})
.populate('datasetId')
.lean();
if (collection) {
return {
...collection,
dataset: collection.datasetId as unknown as DatasetSchemaType
};
}
return Promise.reject(ERROR_ENUM.unAuthDataset);
}

View File

@@ -0,0 +1,66 @@
import { connectionMongo, type Model } from '../../../common/mongo';
const { Schema, model, models } = connectionMongo;
import { DatasetCollectionSchemaType } from '@fastgpt/global/core/dataset/type.d';
import { DatasetCollectionTypeMap } from '@fastgpt/global/core/dataset/constant';
import { DatasetCollectionName } from '../schema';
export const DatasetColCollectionName = 'dataset.collections';
const DatasetCollectionSchema = new Schema({
parentId: {
type: Schema.Types.ObjectId,
ref: DatasetColCollectionName,
default: null
},
userId: {
type: Schema.Types.ObjectId,
ref: 'user',
required: true
},
datasetId: {
type: Schema.Types.ObjectId,
ref: DatasetCollectionName,
required: true
},
name: {
type: String,
required: true
},
type: {
type: String,
enum: Object.keys(DatasetCollectionTypeMap),
required: true
},
updateTime: {
type: Date,
default: () => new Date()
},
metadata: {
type: {
fileId: {
type: Schema.Types.ObjectId,
ref: 'dataset.files'
},
rawLink: {
type: String,
default: ''
},
// 451 初始化
pgCollectionId: {
type: String
}
},
default: {}
}
});
try {
DatasetCollectionSchema.index({ datasetId: 1 });
DatasetCollectionSchema.index({ userId: 1 });
DatasetCollectionSchema.index({ updateTime: -1 });
} catch (error) {
console.log(error);
}
export const MongoDatasetCollection: Model<DatasetCollectionSchemaType> =
models[DatasetColCollectionName] || model(DatasetColCollectionName, DatasetCollectionSchema);

View File

@@ -0,0 +1,62 @@
import { MongoDatasetCollection } from './schema';
import { ParentTreePathItemType } from '@fastgpt/global/common/parentFolder/type';
/**
* get all collection by top collectionId
*/
export async function findCollectionAndChild(id: string, fields = '_id parentId name metadata') {
async function find(id: string) {
// find children
const children = await MongoDatasetCollection.find({ parentId: id }, fields);
let collections = children;
for (const child of children) {
const grandChildrenIds = await find(child._id);
collections = collections.concat(grandChildrenIds);
}
return collections;
}
const [collection, childCollections] = await Promise.all([
MongoDatasetCollection.findById(id, fields),
find(id)
]);
if (!collection) {
return Promise.reject('Collection not found');
}
return [collection, ...childCollections];
}
export async function getDatasetCollectionPaths({
parentId = '',
userId
}: {
parentId?: string;
userId: string;
}): Promise<ParentTreePathItemType[]> {
async function find(parentId?: string): Promise<ParentTreePathItemType[]> {
if (!parentId) {
return [];
}
const parent = await MongoDatasetCollection.findOne({ _id: parentId, userId }, 'name parentId');
if (!parent) return [];
const paths = await find(parent.parentId);
paths.push({ parentId, parentName: parent.name });
return paths;
}
return await find(parentId);
}
export function getCollectionUpdateTime({ name, time }: { time?: Date; name: string }) {
if (time) return time;
if (name.startsWith('手动') || ['manual', 'mark'].includes(name)) return new Date('2999/9/9');
return new Date();
}

View File

@@ -0,0 +1,55 @@
import { connectionMongo, type Model } from '../../common/mongo';
const { Schema, model, models } = connectionMongo;
import { DatasetSchemaType } from '@fastgpt/global/core/dataset/type.d';
import { DatasetTypeMap } from '@fastgpt/global/core/dataset/constant';
export const DatasetCollectionName = 'datasets';
const DatasetSchema = new Schema({
parentId: {
type: Schema.Types.ObjectId,
ref: DatasetCollectionName,
default: null
},
userId: {
type: Schema.Types.ObjectId,
ref: 'user',
required: true
},
updateTime: {
type: Date,
default: () => new Date()
},
avatar: {
type: String,
default: '/icon/logo.svg'
},
name: {
type: String,
required: true
},
vectorModel: {
type: String,
required: true,
default: 'text-embedding-ada-002'
},
type: {
type: String,
enum: Object.keys(DatasetTypeMap),
required: true,
default: 'dataset'
},
tags: {
type: [String],
default: []
}
});
try {
DatasetSchema.index({ userId: 1 });
} catch (error) {
console.log(error);
}
export const MongoDataset: Model<DatasetSchemaType> =
models[DatasetCollectionName] || model(DatasetCollectionName, DatasetSchema);

View File

@@ -0,0 +1,72 @@
/* 模型的知识库 */
import { connectionMongo, type Model } from '../../../common/mongo';
const { Schema, model, models } = connectionMongo;
import { DatasetTrainingSchemaType } from '@fastgpt/global/core/dataset/type';
import { TrainingTypeMap } from '@fastgpt/global/core/dataset/constant';
import { DatasetColCollectionName } from '../collection/schema';
import { DatasetCollectionName } from '../schema';
export const DatasetTrainingCollectionName = 'dataset.trainings';
const TrainingDataSchema = new Schema({
userId: {
type: Schema.Types.ObjectId,
ref: 'user',
required: true
},
datasetId: {
type: Schema.Types.ObjectId,
ref: DatasetCollectionName,
required: true
},
datasetCollectionId: {
type: Schema.Types.ObjectId,
ref: DatasetColCollectionName,
required: true
},
billId: {
type: String,
default: ''
},
mode: {
type: String,
enum: Object.keys(TrainingTypeMap),
required: true
},
expireAt: {
type: Date,
default: () => new Date()
},
lockTime: {
type: Date,
default: () => new Date('2000/1/1')
},
model: {
type: String,
required: true
},
prompt: {
// qa split prompt
type: String,
default: ''
},
q: {
type: String,
required: true
},
a: {
type: String,
default: ''
}
});
try {
TrainingDataSchema.index({ lockTime: 1 });
TrainingDataSchema.index({ userId: 1 });
TrainingDataSchema.index({ expireAt: 1 }, { expireAfterSeconds: 7 * 24 * 60 });
} catch (error) {
console.log(error);
}
export const MongoDatasetTraining: Model<DatasetTrainingSchemaType> =
models[DatasetTrainingCollectionName] || model(DatasetTrainingCollectionName, TrainingDataSchema);

View File

@@ -0,0 +1,24 @@
{
"name": "@fastgpt/service",
"version": "1.0.0",
"dependencies": {
"@fastgpt/global": "workspace:*",
"axios": "^1.5.1",
"nextjs-cors": "^2.1.2",
"next": "13.5.2",
"cookie": "^0.5.0",
"jsonwebtoken": "^9.0.2",
"mongoose": "^7.0.2",
"winston": "^3.10.0",
"winston-mongodb": "^5.1.1",
"tunnel": "^0.0.6",
"encoding": "^0.1.13",
"openai": "^4.12.1"
},
"devDependencies": {
"@types/tunnel": "^0.0.4",
"@types/node": "^20.8.5",
"@types/cookie": "^0.5.2",
"@types/jsonwebtoken": "^9.0.3"
}
}

View File

@@ -0,0 +1,32 @@
import { ERROR_ENUM } from '@fastgpt/global/common/error/errorCode';
import { updateApiKeyUsedTime } from './tools';
import { MongoOpenApi } from './schema';
import { POST } from '../../common/api/plusRequest';
import type { OpenApiSchema } from '@fastgpt/global/support/openapi/type';
export type AuthOpenApiLimitProps = { openApi: OpenApiSchema };
export async function authOpenApiKey({ apikey }: { apikey: string }) {
if (!apikey) {
return Promise.reject(ERROR_ENUM.unAuthApiKey);
}
try {
const openApi = await MongoOpenApi.findOne({ apiKey: apikey });
if (!openApi) {
return Promise.reject(ERROR_ENUM.unAuthApiKey);
}
const userId = String(openApi.userId);
// auth limit
if (global.feConfigs?.isPlus) {
await POST('/support/openapi/authLimit', { openApi } as AuthOpenApiLimitProps);
}
updateApiKeyUsedTime(openApi._id);
return { apikey, userId, appId: openApi.appId };
} catch (error) {
return Promise.reject(error);
}
}

View File

@@ -0,0 +1,59 @@
import { connectionMongo, type Model } from '../../common/mongo';
const { Schema, model, models } = connectionMongo;
import type { OpenApiSchema } from '@fastgpt/global/support/openapi/type';
import { PRICE_SCALE } from '@fastgpt/global/common/bill/constants';
import { formatPrice } from '@fastgpt/global/common/bill/tools';
const OpenApiSchema = new Schema(
{
userId: {
type: Schema.Types.ObjectId,
ref: 'user',
required: true
},
apiKey: {
type: String,
required: true,
get: (val: string) => `******${val.substring(val.length - 4)}`
},
createTime: {
type: Date,
default: () => new Date()
},
lastUsedTime: {
type: Date
},
appId: {
type: String,
required: false
},
name: {
type: String,
default: 'Api Key'
},
usage: {
// total usage. value from bill total
type: Number,
default: 0,
get: (val: number) => formatPrice(val)
},
limit: {
expiredTime: {
type: Date
},
credit: {
// value from user settings
type: Number,
default: -1,
set: (val: number) => val * PRICE_SCALE,
get: (val: number) => formatPrice(val)
}
}
},
{
toObject: { getters: true }
}
);
export const MongoOpenApi: Model<OpenApiSchema> =
models['openapi'] || model('openapi', OpenApiSchema);

View File

@@ -0,0 +1,18 @@
import { MongoOpenApi } from './schema';
export async function updateApiKeyUsedTime(id: string) {
await MongoOpenApi.findByIdAndUpdate(id, {
lastUsedTime: new Date()
});
}
export async function updateApiKeyUsage({ apikey, usage }: { apikey: string; usage: number }) {
await MongoOpenApi.findOneAndUpdate(
{ apiKey: apikey },
{
$inc: {
usage
}
}
);
}

View File

@@ -0,0 +1,68 @@
import { AuthUserTypeEnum, authBalanceByUid } from '../user/auth';
import { MongoOutLink } from './schema';
import { POST } from '../../common/api/plusRequest';
import { OutLinkSchema } from '@fastgpt/global/support/outLink/type';
export type AuthLinkProps = { ip?: string | null; authToken?: string; question: string };
export type AuthLinkLimitProps = AuthLinkProps & { outLink: OutLinkSchema };
export async function authOutLinkChat({
shareId,
ip,
authToken,
question
}: AuthLinkProps & {
shareId: string;
}) {
// get outLink
const outLink = await MongoOutLink.findOne({
shareId
});
if (!outLink) {
return Promise.reject('分享链接无效');
}
const uid = String(outLink.userId);
const [user] = await Promise.all([
authBalanceByUid(uid), // authBalance
...(global.feConfigs?.isPlus ? [authOutLinkLimit({ outLink, ip, authToken, question })] : []) // limit auth
]);
return {
user,
userId: String(outLink.userId),
appId: String(outLink.appId),
authType: AuthUserTypeEnum.token,
responseDetail: outLink.responseDetail
};
}
export function authOutLinkLimit(data: AuthLinkLimitProps) {
return POST('/support/outLink/authLimit', data);
}
export async function authOutLinkId({ id }: { id: string }) {
const outLink = await MongoOutLink.findOne({
shareId: id
});
if (!outLink) {
return Promise.reject('分享链接无效');
}
return {
userId: String(outLink.userId)
};
}
export type AuthShareChatInitProps = {
authToken?: string;
tokenUrl?: string;
};
export function authShareChatInit(data: AuthShareChatInitProps) {
if (!global.feConfigs?.isPlus) return;
return POST('/support/outLink/authShareChatInit', data);
}

View File

@@ -0,0 +1,60 @@
import { connectionMongo, type Model } from '../../common/mongo';
const { Schema, model, models } = connectionMongo;
import { OutLinkSchema as SchemaType } from '@fastgpt/global/support/outLink/type';
import { OutLinkTypeEnum } from '@fastgpt/global/support/outLink/constant';
const OutLinkSchema = new Schema({
shareId: {
type: String,
required: true
},
userId: {
type: Schema.Types.ObjectId,
ref: 'user',
required: true
},
appId: {
type: Schema.Types.ObjectId,
ref: 'model',
required: true
},
type: {
type: String,
default: OutLinkTypeEnum.share
},
name: {
type: String,
required: true
},
total: {
// total amount
type: Number,
default: 0
},
lastTime: {
type: Date
},
responseDetail: {
type: Boolean,
default: false
},
limit: {
expiredTime: {
type: Date
},
QPM: {
type: Number,
default: 1000
},
credit: {
type: Number,
default: -1
},
hookUrl: {
type: String
}
}
});
export const MongoOutLink: Model<SchemaType> =
models['outlinks'] || model('outlinks', OutLinkSchema);

View File

@@ -0,0 +1,50 @@
import axios from 'axios';
import { MongoOutLink } from './schema';
export const updateOutLinkUsage = async ({
shareId,
total
}: {
shareId: string;
total: number;
}) => {
try {
await MongoOutLink.findOneAndUpdate(
{ shareId },
{
$inc: { total },
lastTime: new Date()
}
);
} catch (err) {
console.log('update shareChat error', err);
}
};
export const pushResult2Remote = async ({
authToken,
shareId,
responseData
}: {
authToken?: string;
shareId?: string;
responseData?: any[];
}) => {
if (!shareId || !authToken) return;
try {
const outLink = await MongoOutLink.findOne({
shareId
});
if (!outLink?.limit?.hookUrl) return;
axios({
method: 'post',
baseURL: outLink.limit.hookUrl,
url: '/shareAuth/finish',
data: {
token: authToken,
responseData
}
});
} catch (error) {}
};

View File

@@ -0,0 +1,206 @@
import type { NextApiResponse, NextApiRequest } from 'next';
import Cookie from 'cookie';
import jwt from 'jsonwebtoken';
import { authOpenApiKey } from '../openapi/auth';
import { authOutLinkId } from '../outLink/auth';
import { MongoUser } from './schema';
import type { UserModelSchema } from '@fastgpt/global/support/user/type';
import { ERROR_ENUM } from '@fastgpt/global/common/error/errorCode';
export enum AuthUserTypeEnum {
token = 'token',
root = 'root',
apikey = 'apikey',
outLink = 'outLink'
}
/* auth balance */
export const authBalanceByUid = async (uid: string) => {
const user = await MongoUser.findById<UserModelSchema>(
uid,
'_id username balance openaiAccount timezone'
);
if (!user) {
return Promise.reject(ERROR_ENUM.unAuthorization);
}
if (user.balance <= 0) {
return Promise.reject(ERROR_ENUM.insufficientQuota);
}
return user;
};
/* uniform auth user */
export const authUser = async ({
req,
authToken = false,
authRoot = false,
authApiKey = false,
authBalance = false,
authOutLink
}: {
req: NextApiRequest;
authToken?: boolean;
authRoot?: boolean;
authApiKey?: boolean;
authBalance?: boolean;
authOutLink?: boolean;
}) => {
const authCookieToken = async (cookie?: string, token?: string): Promise<string> => {
// 获取 cookie
const cookies = Cookie.parse(cookie || '');
const cookieToken = cookies.token || token;
if (!cookieToken) {
return Promise.reject(ERROR_ENUM.unAuthorization);
}
return await authJWT(cookieToken);
};
// from authorization get apikey
const parseAuthorization = async (authorization?: string) => {
if (!authorization) {
return Promise.reject(ERROR_ENUM.unAuthorization);
}
// Bearer fastgpt-xxxx-appId
const auth = authorization.split(' ')[1];
if (!auth) {
return Promise.reject(ERROR_ENUM.unAuthorization);
}
const { apikey, appId: authorizationAppid = '' } = await (async () => {
const arr = auth.split('-');
// abandon
if (arr.length === 3) {
return {
apikey: `${arr[0]}-${arr[1]}`,
appId: arr[2]
};
}
if (arr.length === 2) {
return {
apikey: auth
};
}
return Promise.reject(ERROR_ENUM.unAuthorization);
})();
// auth apikey
const { userId, appId: apiKeyAppId = '' } = await authOpenApiKey({ apikey });
return {
uid: userId,
apikey,
appId: apiKeyAppId || authorizationAppid
};
};
// root user
const parseRootKey = async (rootKey?: string, userId = '') => {
if (!rootKey || !process.env.ROOT_KEY || rootKey !== process.env.ROOT_KEY) {
return Promise.reject(ERROR_ENUM.unAuthorization);
}
return userId;
};
const { cookie, token, apikey, rootkey, userid, authorization } = (req.headers || {}) as {
cookie?: string;
token?: string;
apikey?: string;
rootkey?: string; // abandon
userid?: string;
authorization?: string;
};
const { shareId } = (req?.body || {}) as { shareId?: string };
let uid = '';
let appId = '';
let openApiKey = apikey;
let authType: `${AuthUserTypeEnum}` = AuthUserTypeEnum.token;
if (authOutLink && shareId) {
const res = await authOutLinkId({ id: shareId });
uid = res.userId;
authType = AuthUserTypeEnum.outLink;
} else if (authToken && (cookie || token)) {
// user token(from fastgpt web)
uid = await authCookieToken(cookie, token);
authType = AuthUserTypeEnum.token;
} else if (authRoot && rootkey) {
// root user
uid = await parseRootKey(rootkey, userid);
authType = AuthUserTypeEnum.root;
} else if (authApiKey && apikey) {
// apikey
const parseResult = await authOpenApiKey({ apikey });
uid = parseResult.userId;
authType = AuthUserTypeEnum.apikey;
openApiKey = parseResult.apikey;
} else if (authApiKey && authorization) {
// apikey from authorization
const authResponse = await parseAuthorization(authorization);
uid = authResponse.uid;
appId = authResponse.appId;
openApiKey = authResponse.apikey;
authType = AuthUserTypeEnum.apikey;
}
// not rootUser and no uid, reject request
if (!rootkey && !uid) {
return Promise.reject(ERROR_ENUM.unAuthorization);
}
// balance check
const user = await (() => {
if (authBalance) {
return authBalanceByUid(uid);
}
})();
return {
userId: String(uid),
appId,
authType,
user,
apikey: openApiKey
};
};
/* 生成 token */
export function 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;
}
// auth token
export function authJWT(token: string) {
return new Promise<string>((resolve, reject) => {
const key = process.env.TOKEN_KEY as string;
jwt.verify(token, key, function (err, decoded: any) {
if (err || !decoded?.userId) {
reject(ERROR_ENUM.unAuthorization);
return;
}
resolve(decoded.userId);
});
});
}
/* set cookie */
export const setCookie = (res: NextApiResponse, token: string) => {
res.setHeader(
'Set-Cookie',
`token=${token}; Path=/; HttpOnly; Max-Age=604800; Samesite=None; Secure;`
);
};
/* clear cookie */
export const clearCookie = (res: NextApiResponse) => {
res.setHeader('Set-Cookie', 'token=; Path=/; Max-Age=0');
};

View File

@@ -0,0 +1,63 @@
import { connectionMongo, type Model } from '../../common/mongo';
const { Schema, model, models } = connectionMongo;
import { hashStr } from '@fastgpt/global/common/string/tools';
import { PRICE_SCALE } from '@fastgpt/global/common/bill/constants';
import type { UserModelSchema } from '@fastgpt/global/support/user/type';
const UserSchema = new Schema({
username: {
// 可以是手机/邮箱,新的验证都只用手机
type: String,
required: true,
unique: true // 唯一
},
password: {
type: String,
required: true,
set: (val: string) => hashStr(val),
get: (val: string) => hashStr(val),
select: false
},
createTime: {
type: Date,
default: () => new Date()
},
avatar: {
type: String,
default: '/icon/human.svg'
},
balance: {
type: Number,
default: 2 * PRICE_SCALE
},
inviterId: {
// 谁邀请注册的
type: Schema.Types.ObjectId,
ref: 'user'
},
promotionRate: {
type: Number,
default: 15
},
limit: {
exportKbTime: {
// Every half hour
type: Date
},
datasetMaxCount: {
type: Number
}
},
openaiAccount: {
type: {
key: String,
baseUrl: String
}
},
timezone: {
type: String,
default: 'Asia/Shanghai'
}
});
export const MongoUser: Model<UserModelSchema> = models['user'] || model('user', UserSchema);

View File

@@ -0,0 +1,21 @@
{
"compilerOptions": {
"target": "es2015",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"baseUrl": "."
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "**/*.d.ts", "../**/*.d.ts"],
"exclude": ["node_modules"]
}