mirror of
https://github.com/labring/FastGPT.git
synced 2025-07-21 11:43:56 +00:00
Extraction schema (#398)
This commit is contained in:
78
packages/common/constant/errorCode.ts
Normal file
78
packages/common/constant/errorCode.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
export const ERROR_CODE: { [key: number]: string } = {
|
||||
400: '请求失败',
|
||||
401: '无权访问',
|
||||
403: '紧张访问',
|
||||
404: '请求不存在',
|
||||
405: '请求方法错误',
|
||||
406: '请求的格式错误',
|
||||
410: '资源已删除',
|
||||
422: '验证错误',
|
||||
500: '服务器发生错误',
|
||||
502: '网关错误',
|
||||
503: '服务器暂时过载或维护',
|
||||
504: '网关超时'
|
||||
};
|
||||
|
||||
export const TOKEN_ERROR_CODE: Record<number, string> = {
|
||||
403: '登录状态无效,请重新登录'
|
||||
};
|
||||
|
||||
export const proxyError: Record<string, boolean> = {
|
||||
ECONNABORTED: true,
|
||||
ECONNRESET: true
|
||||
};
|
||||
|
||||
export enum ERROR_ENUM {
|
||||
unAuthorization = 'unAuthorization',
|
||||
insufficientQuota = 'insufficientQuota',
|
||||
unAuthModel = 'unAuthModel',
|
||||
unAuthApiKey = 'unAuthApiKey',
|
||||
unAuthKb = 'unAuthKb',
|
||||
unAuthFile = 'unAuthFile'
|
||||
}
|
||||
export const ERROR_RESPONSE: Record<
|
||||
any,
|
||||
{
|
||||
code: number;
|
||||
statusText: string;
|
||||
message: string;
|
||||
data?: any;
|
||||
}
|
||||
> = {
|
||||
[ERROR_ENUM.unAuthorization]: {
|
||||
code: 403,
|
||||
statusText: ERROR_ENUM.unAuthorization,
|
||||
message: '凭证错误',
|
||||
data: null
|
||||
},
|
||||
[ERROR_ENUM.insufficientQuota]: {
|
||||
code: 510,
|
||||
statusText: ERROR_ENUM.insufficientQuota,
|
||||
message: '账号余额不足',
|
||||
data: null
|
||||
},
|
||||
[ERROR_ENUM.unAuthModel]: {
|
||||
code: 511,
|
||||
statusText: ERROR_ENUM.unAuthModel,
|
||||
message: '无权使用该模型',
|
||||
data: null
|
||||
},
|
||||
[ERROR_ENUM.unAuthKb]: {
|
||||
code: 512,
|
||||
statusText: ERROR_ENUM.unAuthKb,
|
||||
message: '无权使用该知识库',
|
||||
data: null
|
||||
},
|
||||
[ERROR_ENUM.unAuthFile]: {
|
||||
code: 513,
|
||||
statusText: ERROR_ENUM.unAuthFile,
|
||||
message: '无权阅读该文件',
|
||||
data: null
|
||||
},
|
||||
[ERROR_ENUM.unAuthApiKey]: {
|
||||
code: 514,
|
||||
statusText: ERROR_ENUM.unAuthApiKey,
|
||||
message: 'Api Key 不合法',
|
||||
data: null
|
||||
}
|
||||
};
|
6
packages/common/mongo/index.ts
Normal file
6
packages/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;
|
74
packages/common/mongo/init.ts
Normal file
74
packages/common/mongo/init.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
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
|
||||
});
|
||||
|
||||
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)}` : ''
|
||||
}`;
|
||||
})
|
||||
)
|
||||
})
|
||||
]
|
||||
});
|
||||
}
|
@@ -1,4 +1,13 @@
|
||||
{
|
||||
"name": "@fastgpt/common",
|
||||
"version": "1.0.0"
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"mongoose": "^7.0.2",
|
||||
"winston": "^3.10.0",
|
||||
"winston-mongodb": "^5.1.1",
|
||||
"axios": "^1.5.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.8.5"
|
||||
}
|
||||
}
|
||||
|
15
packages/common/plusApi/censor.ts
Normal file
15
packages/common/plusApi/censor.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { POST } from './request';
|
||||
|
||||
export const postTextCensor = (data: { text: string }) =>
|
||||
POST<{ code?: number; message: string }>('/common/censor/text_baidu', data)
|
||||
.then((res) => {
|
||||
if (res?.code === 5000) {
|
||||
return Promise.reject(res);
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
if (err?.code === 5000) {
|
||||
return Promise.reject(err.message);
|
||||
}
|
||||
return Promise.resolve('');
|
||||
});
|
120
packages/common/plusApi/request.ts
Normal file
120
packages/common/plusApi/request.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');
|
||||
}
|
@@ -1,5 +1,32 @@
|
||||
import crypto from 'crypto';
|
||||
|
||||
export function strIsLink(str?: string) {
|
||||
if (!str) return false;
|
||||
if (/^((http|https)?:\/\/|www\.|\/)[^\s/$.?#].[^\s]*$/i.test(str)) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
export const hashStr = (psw: string) => {
|
||||
return crypto.createHash('sha256').update(psw).digest('hex');
|
||||
};
|
||||
|
||||
/* simple text, remove chinese space and extra \n */
|
||||
export const simpleText = (text: string) => {
|
||||
text = text.replace(/([\u4e00-\u9fa5])[\s&&[^\n]]+([\u4e00-\u9fa5])/g, '$1$2');
|
||||
text = text.replace(/\n{2,}/g, '\n');
|
||||
text = text.replace(/[\s&&[^\n]]{2,}/g, ' ');
|
||||
text = text.replace(/[\x00-\x08]/g, ' ');
|
||||
|
||||
// replace empty \n
|
||||
let newText = '';
|
||||
let lastChar = '';
|
||||
for (let i = 0; i < text.length; i++) {
|
||||
const currentChar = text[i];
|
||||
if (currentChar === '\n' && !/[。?!;.?!;]/g.test(lastChar)) {
|
||||
} else {
|
||||
newText += currentChar;
|
||||
}
|
||||
lastChar = currentChar;
|
||||
}
|
||||
return newText;
|
||||
};
|
||||
|
0
packages/common/type/chat.d.ts
vendored
Normal file
0
packages/common/type/chat.d.ts
vendored
Normal file
43
packages/common/type/index.d.ts
vendored
Normal file
43
packages/common/type/index.d.ts
vendored
Normal file
@@ -0,0 +1,43 @@
|
||||
import type { Mongoose } from '../mongo';
|
||||
import type { Logger } from 'winston';
|
||||
|
||||
export type FeConfigsType = {
|
||||
show_emptyChat?: boolean;
|
||||
show_register?: boolean;
|
||||
show_appStore?: boolean;
|
||||
show_contact?: boolean;
|
||||
show_git?: boolean;
|
||||
show_doc?: boolean;
|
||||
show_pay?: boolean;
|
||||
show_openai_account?: boolean;
|
||||
show_promotion?: boolean;
|
||||
hide_app_flow?: boolean;
|
||||
openAPIUrl?: string;
|
||||
systemTitle?: string;
|
||||
authorText?: string;
|
||||
googleClientVerKey?: string;
|
||||
isPlus?: boolean;
|
||||
oauth?: {
|
||||
github?: string;
|
||||
google?: string;
|
||||
};
|
||||
limit?: {
|
||||
exportLimitMinutes?: number;
|
||||
};
|
||||
scripts?: { [key: string]: string }[];
|
||||
};
|
||||
|
||||
export type SystemEnvType = {
|
||||
pluginBaseUrl?: string;
|
||||
openapiPrefix?: string;
|
||||
vectorMaxProcess: number;
|
||||
qaMaxProcess: number;
|
||||
pgHNSWEfSearch: number;
|
||||
};
|
||||
|
||||
declare global {
|
||||
var mongodb: Mongoose | undefined;
|
||||
var logger: Logger;
|
||||
var feConfigs: FeConfigsType;
|
||||
var systemEnv: SystemEnvType;
|
||||
}
|
@@ -1,4 +1,4 @@
|
||||
import { UserModelSchema } from '../user/type';
|
||||
import type { UserModelSchema } from '@fastgpt/support/user/type.d';
|
||||
import OpenAI from 'openai';
|
||||
|
||||
export const openaiBaseUrl = process.env.OPENAI_BASE_URL || 'https://api.openai.com/v1';
|
||||
|
@@ -10,7 +10,7 @@ export async function createQuestionGuide({
|
||||
messages: ChatCompletionRequestMessage[];
|
||||
model: string;
|
||||
}) {
|
||||
const ai = getAIApi();
|
||||
const ai = getAIApi(undefined, 48000);
|
||||
const data = await ai.chat.completions.create({
|
||||
model: model,
|
||||
temperature: 0,
|
||||
@@ -25,7 +25,7 @@ export async function createQuestionGuide({
|
||||
stream: false
|
||||
});
|
||||
|
||||
const answer = data.choices?.[0].message?.content || '';
|
||||
const answer = data.choices?.[0]?.message?.content || '';
|
||||
const totalTokens = data.usage?.total_tokens || 0;
|
||||
|
||||
const start = answer.indexOf('[');
|
||||
|
0
packages/core/chat/type.d.ts
vendored
Normal file
0
packages/core/chat/type.d.ts
vendored
Normal file
@@ -1,3 +1,22 @@
|
||||
export enum DatasetTypeEnum {
|
||||
folder = 'folder',
|
||||
dataset = 'dataset'
|
||||
}
|
||||
|
||||
export const DatasetTypeMap = {
|
||||
[DatasetTypeEnum.folder]: {
|
||||
name: 'folder'
|
||||
},
|
||||
[DatasetTypeEnum.dataset]: {
|
||||
name: 'dataset'
|
||||
}
|
||||
};
|
||||
|
||||
export enum FileStatusEnum {
|
||||
embedding = 'embedding',
|
||||
ready = 'ready'
|
||||
}
|
||||
|
||||
export enum DatasetSpecialIdEnum {
|
||||
manual = 'manual',
|
||||
mark = 'mark'
|
||||
@@ -13,3 +32,5 @@ export const datasetSpecialIdMap = {
|
||||
}
|
||||
};
|
||||
export const datasetSpecialIds: string[] = [DatasetSpecialIdEnum.manual, DatasetSpecialIdEnum.mark];
|
||||
|
||||
export const FolderAvatarSrc = '/imgs/files/folder.svg';
|
||||
|
46
packages/core/dataset/schema.ts
Normal file
46
packages/core/dataset/schema.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import { connectionMongo, type Model } from '@fastgpt/common/mongo';
|
||||
const { Schema, model, models } = connectionMongo;
|
||||
import { DatasetSchemaType } from './type';
|
||||
import { DatasetTypeMap } from './constant';
|
||||
|
||||
const DatasetSchema = new Schema({
|
||||
parentId: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'kb',
|
||||
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: []
|
||||
}
|
||||
});
|
||||
|
||||
export const MongoDataset: Model<DatasetSchemaType> = models['kb'] || model('kb', DatasetSchema);
|
13
packages/core/dataset/type.d.ts
vendored
Normal file
13
packages/core/dataset/type.d.ts
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
import { DatasetTypeEnum } from './constant';
|
||||
|
||||
export type DatasetSchemaType = {
|
||||
_id: string;
|
||||
userId: string;
|
||||
parentId: string;
|
||||
updateTime: Date;
|
||||
avatar: string;
|
||||
name: string;
|
||||
vectorModel: string;
|
||||
tags: string[];
|
||||
type: `${DatasetTypeEnum}`;
|
||||
};
|
@@ -16,6 +16,6 @@
|
||||
"incremental": true,
|
||||
"baseUrl": "."
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "**/*.d.ts"],
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "**/*.d.ts", "../**/*.d.ts"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
|
32
packages/support/openapi/auth.ts
Normal file
32
packages/support/openapi/auth.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { ERROR_ENUM } from '@fastgpt/common/constant/errorCode';
|
||||
import { updateApiKeyUsedTime } from './tools';
|
||||
import { MongoOpenApi } from './schema';
|
||||
import { POST } from '@fastgpt/common/plusApi/request';
|
||||
import { OpenApiSchema } from './type.d';
|
||||
|
||||
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/support/openapi/schema.ts
Normal file
59
packages/support/openapi/schema.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import { connectionMongo, type Model } from '@fastgpt/common/mongo';
|
||||
const { Schema, model, models } = connectionMongo;
|
||||
import { OpenApiSchema } from './type.d';
|
||||
import { PRICE_SCALE } from '@fastgpt/common/bill/constants';
|
||||
import { formatPrice } from '@fastgpt/common/bill/index';
|
||||
|
||||
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/support/openapi/tools.ts
Normal file
18
packages/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
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
14
packages/support/openapi/type.d.ts
vendored
Normal file
14
packages/support/openapi/type.d.ts
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
export type OpenApiSchema = {
|
||||
_id: string;
|
||||
userId: string;
|
||||
createTime: Date;
|
||||
lastUsedTime?: Date;
|
||||
apiKey: string;
|
||||
appId?: string;
|
||||
name: string;
|
||||
usage: number;
|
||||
limit?: {
|
||||
expiredTime?: Date;
|
||||
credit?: number;
|
||||
};
|
||||
};
|
67
packages/support/outLink/auth.ts
Normal file
67
packages/support/outLink/auth.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import { AuthUserTypeEnum, authBalanceByUid } from '../user/auth';
|
||||
import { MongoOutLink } from './schema';
|
||||
import { POST } from '@fastgpt/common/plusApi/request';
|
||||
import { OutLinkSchema } from './type.d';
|
||||
|
||||
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) {
|
||||
return POST('/support/outLink/authShareChatInit', data);
|
||||
}
|
5
packages/support/outLink/constant.ts
Normal file
5
packages/support/outLink/constant.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export enum OutLinkTypeEnum {
|
||||
'share' = 'share',
|
||||
'iframe' = 'iframe',
|
||||
apikey = 'apikey'
|
||||
}
|
60
packages/support/outLink/schema.ts
Normal file
60
packages/support/outLink/schema.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import { connectionMongo, type Model } from '@fastgpt/common/mongo';
|
||||
const { Schema, model, models } = connectionMongo;
|
||||
import { OutLinkSchema as SchemaType } from './type.d';
|
||||
import { OutLinkTypeEnum } from './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/support/outLink/tools.ts
Normal file
50
packages/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) {}
|
||||
};
|
26
packages/support/outLink/type.d.ts
vendored
Normal file
26
packages/support/outLink/type.d.ts
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
import { OutLinkTypeEnum } from './constant';
|
||||
|
||||
export type OutLinkSchema = {
|
||||
_id: string;
|
||||
shareId: string;
|
||||
userId: string;
|
||||
appId: string;
|
||||
name: string;
|
||||
total: number;
|
||||
lastTime: Date;
|
||||
type: `${OutLinkTypeEnum}`;
|
||||
responseDetail: boolean;
|
||||
limit?: {
|
||||
expiredTime?: Date;
|
||||
QPM: number;
|
||||
credit: number;
|
||||
hookUrl?: string;
|
||||
};
|
||||
};
|
||||
|
||||
export type OutLinkEditType = {
|
||||
_id?: string;
|
||||
name: string;
|
||||
responseDetail: OutLinkSchema['responseDetail'];
|
||||
limit: OutLinkSchema['limit'];
|
||||
};
|
@@ -2,6 +2,13 @@
|
||||
"name": "@fastgpt/support",
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"@fastgpt/common": "workspace:*"
|
||||
"@fastgpt/common": "workspace:*",
|
||||
"cookie": "^0.5.0",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"axios": "^1.5.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/cookie": "^0.5.2",
|
||||
"@types/jsonwebtoken": "^9.0.3"
|
||||
}
|
||||
}
|
||||
|
@@ -14,8 +14,11 @@
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
"incremental": true,
|
||||
"baseUrl": "."
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./*"]
|
||||
}
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "**/*.d.ts"],
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "**/*.d.ts", "../**/*.d.ts"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
|
167
packages/support/user/auth.ts
Normal file
167
packages/support/user/auth.ts
Normal file
@@ -0,0 +1,167 @@
|
||||
import Cookie from 'cookie';
|
||||
import { authJWT } from './tools';
|
||||
import { authOpenApiKey } from '../openapi/auth';
|
||||
import { authOutLinkId } from '../outLink/auth';
|
||||
|
||||
import { MongoUser } from './schema';
|
||||
import type { UserModelSchema } from './type.d';
|
||||
import { ERROR_ENUM } from '@fastgpt/common/constant/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: any;
|
||||
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
|
||||
};
|
||||
};
|
63
packages/support/user/schema.ts
Normal file
63
packages/support/user/schema.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import { connectionMongo, type Model } from '@fastgpt/common/mongo';
|
||||
const { Schema, model, models } = connectionMongo;
|
||||
import { hashStr } from '@fastgpt/common/tools/str';
|
||||
import { PRICE_SCALE } from '@fastgpt/common/bill/constants';
|
||||
import type { UserModelSchema } from './type.d';
|
||||
|
||||
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);
|
28
packages/support/user/tools.ts
Normal file
28
packages/support/user/tools.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import jwt from 'jsonwebtoken';
|
||||
import { ERROR_ENUM } from '@fastgpt/common/constant/errorCode';
|
||||
|
||||
/* 生成 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;
|
||||
};
|
||||
// auth token
|
||||
export const authJWT = (token: string) =>
|
||||
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);
|
||||
});
|
||||
});
|
@@ -15,5 +15,6 @@ export type UserModelSchema = {
|
||||
};
|
||||
limit: {
|
||||
exportKbTime?: Date;
|
||||
datasetMaxCount?: number;
|
||||
};
|
||||
};
|
Reference in New Issue
Block a user