v4.5.1 (#417)
This commit is contained in:
120
packages/service/common/api/plusRequest.ts
Normal file
120
packages/service/common/api/plusRequest.ts
Normal 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');
|
||||
}
|
||||
19
packages/service/common/middle/cors.ts
Normal file
19
packages/service/common/middle/cors.ts
Normal 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);
|
||||
};
|
||||
}
|
||||
13
packages/service/common/middle/httpAgent.ts
Normal file
13
packages/service/common/middle/httpAgent.ts
Normal 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
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
6
packages/service/common/mongo/index.ts
Normal file
6
packages/service/common/mongo/index.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import mongoose from 'mongoose';
|
||||
|
||||
export default mongoose;
|
||||
export * from 'mongoose';
|
||||
|
||||
export const connectionMongo = global.mongodb || mongoose;
|
||||
76
packages/service/common/mongo/init.ts
Normal file
76
packages/service/common/mongo/init.ts
Normal 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)}` : ''
|
||||
}`;
|
||||
})
|
||||
)
|
||||
})
|
||||
]
|
||||
});
|
||||
}
|
||||
39
packages/service/common/mongo/sessionRun.ts
Normal file
39
packages/service/common/mongo/sessionRun.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
7
packages/service/common/mongo/type.d.ts
vendored
Normal file
7
packages/service/common/mongo/type.d.ts
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
import type { Mongoose } from 'mongoose';
|
||||
import type { Logger } from 'winston';
|
||||
|
||||
declare global {
|
||||
var mongodb: Mongoose | undefined;
|
||||
var logger: Logger;
|
||||
}
|
||||
39
packages/service/common/response/index.ts
Normal file
39
packages/service/common/response/index.ts
Normal 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`);
|
||||
}
|
||||
17
packages/service/core/ai/config.ts
Normal file
17
packages/service/core/ai/config.ts
Normal 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
|
||||
});
|
||||
};
|
||||
57
packages/service/core/ai/functions/createQuestionGuide.ts
Normal file
57
packages/service/core/ai/functions/createQuestionGuide.ts
Normal 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
|
||||
};
|
||||
}
|
||||
}
|
||||
26
packages/service/core/dataset/auth.ts
Normal file
26
packages/service/core/dataset/auth.ts
Normal 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);
|
||||
}
|
||||
66
packages/service/core/dataset/collection/schema.ts
Normal file
66
packages/service/core/dataset/collection/schema.ts
Normal 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);
|
||||
62
packages/service/core/dataset/collection/utils.ts
Normal file
62
packages/service/core/dataset/collection/utils.ts
Normal 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();
|
||||
}
|
||||
55
packages/service/core/dataset/schema.ts
Normal file
55
packages/service/core/dataset/schema.ts
Normal 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);
|
||||
72
packages/service/core/dataset/training/schema.ts
Normal file
72
packages/service/core/dataset/training/schema.ts
Normal 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);
|
||||
24
packages/service/package.json
Normal file
24
packages/service/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
32
packages/service/support/openapi/auth.ts
Normal file
32
packages/service/support/openapi/auth.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
59
packages/service/support/openapi/schema.ts
Normal file
59
packages/service/support/openapi/schema.ts
Normal 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);
|
||||
18
packages/service/support/openapi/tools.ts
Normal file
18
packages/service/support/openapi/tools.ts
Normal 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
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
68
packages/service/support/outLink/auth.ts
Normal file
68
packages/service/support/outLink/auth.ts
Normal 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);
|
||||
}
|
||||
60
packages/service/support/outLink/schema.ts
Normal file
60
packages/service/support/outLink/schema.ts
Normal 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);
|
||||
50
packages/service/support/outLink/tools.ts
Normal file
50
packages/service/support/outLink/tools.ts
Normal 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) {}
|
||||
};
|
||||
206
packages/service/support/user/auth.ts
Normal file
206
packages/service/support/user/auth.ts
Normal 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');
|
||||
};
|
||||
63
packages/service/support/user/schema.ts
Normal file
63
packages/service/support/user/schema.ts
Normal 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);
|
||||
21
packages/service/tsconfig.json
Normal file
21
packages/service/tsconfig.json
Normal 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"]
|
||||
}
|
||||
Reference in New Issue
Block a user