mirror of
https://github.com/labring/FastGPT.git
synced 2025-07-29 09:44:47 +00:00
new framwork
This commit is contained in:
352
client/src/service/utils/auth.ts
Normal file
352
client/src/service/utils/auth.ts
Normal file
@@ -0,0 +1,352 @@
|
||||
import type { NextApiRequest } from 'next';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import Cookie from 'cookie';
|
||||
import { Chat, Model, OpenApi, User, ShareChat, KB } from '../mongo';
|
||||
import type { ModelSchema } from '@/types/mongoSchema';
|
||||
import type { ChatItemSimpleType } from '@/types/chat';
|
||||
import mongoose from 'mongoose';
|
||||
import { ClaudeEnum, defaultModel, embeddingModel, EmbeddingModelType } from '@/constants/model';
|
||||
import { formatPrice } from '@/utils/user';
|
||||
import { ERROR_ENUM } from '../errorCode';
|
||||
import { ChatModelType, OpenAiChatEnum } from '@/constants/model';
|
||||
import { hashPassword } from '@/service/utils/tools';
|
||||
|
||||
export type ApiKeyType = 'training' | 'chat';
|
||||
|
||||
export const parseCookie = (cookie?: string): Promise<string> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
// 获取 cookie
|
||||
const cookies = Cookie.parse(cookie || '');
|
||||
const token = cookies.token;
|
||||
|
||||
if (!token) {
|
||||
return reject(ERROR_ENUM.unAuthorization);
|
||||
}
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/* uniform auth user */
|
||||
export const authUser = async ({
|
||||
req,
|
||||
authToken = false,
|
||||
authOpenApi = false,
|
||||
authRoot = false,
|
||||
authBalance = false
|
||||
}: {
|
||||
req: NextApiRequest;
|
||||
authToken?: boolean;
|
||||
authOpenApi?: boolean;
|
||||
authRoot?: boolean;
|
||||
authBalance?: boolean;
|
||||
}) => {
|
||||
const parseOpenApiKey = async (apiKey?: string) => {
|
||||
if (!apiKey) {
|
||||
return Promise.reject(ERROR_ENUM.unAuthorization);
|
||||
}
|
||||
|
||||
try {
|
||||
const openApi = await OpenApi.findOne({ apiKey });
|
||||
if (!openApi) {
|
||||
return Promise.reject(ERROR_ENUM.unAuthorization);
|
||||
}
|
||||
const userId = String(openApi.userId);
|
||||
|
||||
// 更新使用的时间
|
||||
await OpenApi.findByIdAndUpdate(openApi._id, {
|
||||
lastUsedTime: new Date()
|
||||
});
|
||||
|
||||
return userId;
|
||||
} catch (error) {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
};
|
||||
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, apikey, rootkey, userid } = (req.headers || {}) as {
|
||||
cookie?: string;
|
||||
apikey?: string;
|
||||
rootkey?: string;
|
||||
userid?: string;
|
||||
};
|
||||
|
||||
let uid = '';
|
||||
|
||||
if (authToken) {
|
||||
uid = await parseCookie(cookie);
|
||||
} else if (authOpenApi) {
|
||||
uid = await parseOpenApiKey(apikey);
|
||||
} else if (authRoot) {
|
||||
uid = await parseRootKey(rootkey, userid);
|
||||
} else if (cookie) {
|
||||
uid = await parseCookie(cookie);
|
||||
} else if (apikey) {
|
||||
uid = await parseOpenApiKey(apikey);
|
||||
} else if (rootkey) {
|
||||
uid = await parseRootKey(rootkey, userid);
|
||||
} else {
|
||||
return Promise.reject(ERROR_ENUM.unAuthorization);
|
||||
}
|
||||
|
||||
if (authBalance) {
|
||||
const user = await User.findById(uid);
|
||||
if (!user) {
|
||||
return Promise.reject(ERROR_ENUM.unAuthorization);
|
||||
}
|
||||
|
||||
if (!user.openaiKey && formatPrice(user.balance) <= 0) {
|
||||
return Promise.reject(ERROR_ENUM.insufficientQuota);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
userId: uid
|
||||
};
|
||||
};
|
||||
|
||||
/* random get openai api key */
|
||||
export const getSystemOpenAiKey = (type: ApiKeyType) => {
|
||||
const keys = (() => {
|
||||
if (type === 'training') {
|
||||
return process.env.OPENAI_TRAINING_KEY?.split(',') || [];
|
||||
}
|
||||
return process.env.OPENAIKEY?.split(',') || [];
|
||||
})();
|
||||
|
||||
// 纯字符串类型
|
||||
const i = Math.floor(Math.random() * keys.length);
|
||||
return keys[i] || (process.env.OPENAIKEY as string);
|
||||
};
|
||||
|
||||
/* 获取 api 请求的 key */
|
||||
export const getApiKey = async ({
|
||||
model,
|
||||
userId,
|
||||
mustPay = false,
|
||||
type = 'chat'
|
||||
}: {
|
||||
model: ChatModelType;
|
||||
userId: string;
|
||||
mustPay?: boolean;
|
||||
type?: ApiKeyType;
|
||||
}) => {
|
||||
const user = await User.findById(userId);
|
||||
if (!user) {
|
||||
return Promise.reject(ERROR_ENUM.unAuthorization);
|
||||
}
|
||||
|
||||
const keyMap = {
|
||||
[OpenAiChatEnum.GPT35]: {
|
||||
userOpenAiKey: user.openaiKey || '',
|
||||
systemAuthKey: getSystemOpenAiKey(type) as string
|
||||
},
|
||||
[OpenAiChatEnum.GPT4]: {
|
||||
userOpenAiKey: user.openaiKey || '',
|
||||
systemAuthKey: process.env.GPT4KEY as string
|
||||
},
|
||||
[OpenAiChatEnum.GPT432k]: {
|
||||
userOpenAiKey: user.openaiKey || '',
|
||||
systemAuthKey: process.env.GPT4KEY as string
|
||||
},
|
||||
[ClaudeEnum.Claude]: {
|
||||
userOpenAiKey: '',
|
||||
systemAuthKey: process.env.CLAUDE_KEY as string
|
||||
}
|
||||
};
|
||||
|
||||
// 有自己的key
|
||||
if (!mustPay && keyMap[model].userOpenAiKey) {
|
||||
return {
|
||||
user,
|
||||
userOpenAiKey: keyMap[model].userOpenAiKey,
|
||||
systemAuthKey: ''
|
||||
};
|
||||
}
|
||||
|
||||
// 平台账号余额校验
|
||||
if (formatPrice(user.balance) <= 0) {
|
||||
return Promise.reject(ERROR_ENUM.insufficientQuota);
|
||||
}
|
||||
|
||||
return {
|
||||
user,
|
||||
userOpenAiKey: '',
|
||||
systemAuthKey: keyMap[model].systemAuthKey
|
||||
};
|
||||
};
|
||||
|
||||
// 模型使用权校验
|
||||
export const authModel = async ({
|
||||
modelId,
|
||||
userId,
|
||||
authUser = true,
|
||||
authOwner = true,
|
||||
reserveDetail = false
|
||||
}: {
|
||||
modelId: string;
|
||||
userId: string;
|
||||
authUser?: boolean;
|
||||
authOwner?: boolean;
|
||||
reserveDetail?: boolean; // focus reserve detail
|
||||
}) => {
|
||||
// 获取 model 数据
|
||||
const model = await Model.findById<ModelSchema>(modelId);
|
||||
if (!model) {
|
||||
return Promise.reject('模型不存在');
|
||||
}
|
||||
|
||||
/*
|
||||
Access verification
|
||||
1. authOwner=true or authUser = true , just owner can use
|
||||
2. authUser = false and share, anyone can use
|
||||
*/
|
||||
if (authOwner || (authUser && !model.share.isShare)) {
|
||||
if (userId !== String(model.userId)) return Promise.reject(ERROR_ENUM.unAuthModel);
|
||||
}
|
||||
|
||||
// do not share detail info
|
||||
if (!reserveDetail && !model.share.isShareDetail && userId !== String(model.userId)) {
|
||||
model.chat = {
|
||||
...defaultModel.chat,
|
||||
chatModel: model.chat.chatModel
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
model,
|
||||
showModelDetail: model.share.isShareDetail || userId === String(model.userId)
|
||||
};
|
||||
};
|
||||
|
||||
// 知识库操作权限
|
||||
export const authKb = async ({ kbId, userId }: { kbId: string; userId: string }) => {
|
||||
const kb = await KB.findOne({
|
||||
_id: kbId,
|
||||
userId
|
||||
});
|
||||
if (kb) {
|
||||
return kb;
|
||||
}
|
||||
return Promise.reject(ERROR_ENUM.unAuthKb);
|
||||
};
|
||||
|
||||
// 获取对话校验
|
||||
export const authChat = async ({
|
||||
modelId,
|
||||
chatId,
|
||||
req
|
||||
}: {
|
||||
modelId: string;
|
||||
chatId?: string;
|
||||
req: NextApiRequest;
|
||||
}) => {
|
||||
const { userId } = await authUser({ req, authToken: true });
|
||||
|
||||
// 获取 model 数据
|
||||
const { model, showModelDetail } = await authModel({
|
||||
modelId,
|
||||
userId,
|
||||
authOwner: false,
|
||||
reserveDetail: true
|
||||
});
|
||||
|
||||
// 聊天内容
|
||||
let content: ChatItemSimpleType[] = [];
|
||||
|
||||
if (chatId) {
|
||||
// 获取 chat 数据
|
||||
content = await Chat.aggregate([
|
||||
{ $match: { _id: new mongoose.Types.ObjectId(chatId) } },
|
||||
{
|
||||
$project: {
|
||||
content: {
|
||||
$slice: ['$content', -50] // 返回 content 数组的最后50个元素
|
||||
}
|
||||
}
|
||||
},
|
||||
{ $unwind: '$content' },
|
||||
{
|
||||
$project: {
|
||||
obj: '$content.obj',
|
||||
value: '$content.value',
|
||||
quote: '$content.quote'
|
||||
}
|
||||
}
|
||||
]);
|
||||
}
|
||||
// 获取 user 的 apiKey
|
||||
const { userOpenAiKey, systemAuthKey } = await getApiKey({
|
||||
model: model.chat.chatModel,
|
||||
userId
|
||||
});
|
||||
|
||||
return {
|
||||
userOpenAiKey,
|
||||
systemAuthKey,
|
||||
content,
|
||||
userId,
|
||||
model,
|
||||
showModelDetail
|
||||
};
|
||||
};
|
||||
export const authShareChat = async ({
|
||||
shareId,
|
||||
password
|
||||
}: {
|
||||
shareId: string;
|
||||
password: string;
|
||||
}) => {
|
||||
// get shareChat
|
||||
const shareChat = await ShareChat.findById(shareId);
|
||||
|
||||
if (!shareChat) {
|
||||
return Promise.reject('分享链接已失效');
|
||||
}
|
||||
|
||||
if (shareChat.password !== hashPassword(password)) {
|
||||
return Promise.reject({
|
||||
code: 501,
|
||||
message: '密码不正确'
|
||||
});
|
||||
}
|
||||
|
||||
const modelId = String(shareChat.modelId);
|
||||
const userId = String(shareChat.userId);
|
||||
|
||||
// 获取 model 数据
|
||||
const { model, showModelDetail } = await authModel({
|
||||
modelId,
|
||||
userId,
|
||||
authOwner: false,
|
||||
reserveDetail: true
|
||||
});
|
||||
|
||||
// 获取 user 的 apiKey
|
||||
const { userOpenAiKey, systemAuthKey } = await getApiKey({
|
||||
model: model.chat.chatModel,
|
||||
userId
|
||||
});
|
||||
|
||||
return {
|
||||
userOpenAiKey,
|
||||
systemAuthKey,
|
||||
userId,
|
||||
model,
|
||||
showModelDetail
|
||||
};
|
||||
};
|
77
client/src/service/utils/chat/claude.ts
Normal file
77
client/src/service/utils/chat/claude.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
import { ChatCompletionType, StreamResponseType } from './index';
|
||||
import { ChatRoleEnum } from '@/constants/chat';
|
||||
import axios from 'axios';
|
||||
|
||||
/* 模型对话 */
|
||||
export const claudChat = async ({ apiKey, messages, stream, chatId }: ChatCompletionType) => {
|
||||
// get system prompt
|
||||
const systemPrompt = messages
|
||||
.filter((item) => item.obj === 'System')
|
||||
.map((item) => item.value)
|
||||
.join('\n');
|
||||
const systemPromptText = systemPrompt ? `你本次知识:${systemPrompt}\n下面是我的问题:` : '';
|
||||
|
||||
const prompt = `${systemPromptText}'${messages[messages.length - 1].value}'`;
|
||||
|
||||
const response = await axios.post(
|
||||
process.env.CLAUDE_BASE_URL || '',
|
||||
{
|
||||
prompt,
|
||||
stream,
|
||||
conversationId: chatId
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
Authorization: apiKey
|
||||
},
|
||||
timeout: stream ? 60000 : 240000,
|
||||
responseType: stream ? 'stream' : 'json'
|
||||
}
|
||||
);
|
||||
|
||||
const responseText = stream ? '' : response.data?.text || '';
|
||||
|
||||
return {
|
||||
streamResponse: response,
|
||||
responseMessages: messages.concat({
|
||||
obj: ChatRoleEnum.AI,
|
||||
value: responseText
|
||||
}),
|
||||
responseText,
|
||||
totalTokens: 0
|
||||
};
|
||||
};
|
||||
|
||||
/* openai stream response */
|
||||
export const claudStreamResponse = async ({ res, chatResponse, prompts }: StreamResponseType) => {
|
||||
try {
|
||||
let responseContent = '';
|
||||
|
||||
try {
|
||||
const decoder = new TextDecoder();
|
||||
for await (const chunk of chatResponse.data as any) {
|
||||
if (res.closed) {
|
||||
break;
|
||||
}
|
||||
const content = decoder.decode(chunk);
|
||||
responseContent += content;
|
||||
content && res.write(content);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('pipe error', error);
|
||||
}
|
||||
|
||||
const finishMessages = prompts.concat({
|
||||
obj: ChatRoleEnum.AI,
|
||||
value: responseContent
|
||||
});
|
||||
|
||||
return {
|
||||
responseContent,
|
||||
totalTokens: 0,
|
||||
finishMessages
|
||||
};
|
||||
} catch (error) {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
};
|
167
client/src/service/utils/chat/index.ts
Normal file
167
client/src/service/utils/chat/index.ts
Normal file
@@ -0,0 +1,167 @@
|
||||
import { ChatItemSimpleType } from '@/types/chat';
|
||||
import { modelToolMap } from '@/utils/plugin';
|
||||
import type { ChatModelType } from '@/constants/model';
|
||||
import { ChatRoleEnum } from '@/constants/chat';
|
||||
import { OpenAiChatEnum, ClaudeEnum } from '@/constants/model';
|
||||
import { chatResponse, openAiStreamResponse } from './openai';
|
||||
import { claudChat, claudStreamResponse } from './claude';
|
||||
import type { NextApiResponse } from 'next';
|
||||
|
||||
export type ChatCompletionType = {
|
||||
apiKey: string;
|
||||
temperature: number;
|
||||
messages: ChatItemSimpleType[];
|
||||
chatId?: string;
|
||||
[key: string]: any;
|
||||
};
|
||||
export type ChatCompletionResponseType = {
|
||||
streamResponse: any;
|
||||
responseMessages: ChatItemSimpleType[];
|
||||
responseText: string;
|
||||
totalTokens: number;
|
||||
};
|
||||
export type StreamResponseType = {
|
||||
chatResponse: any;
|
||||
prompts: ChatItemSimpleType[];
|
||||
res: NextApiResponse;
|
||||
[key: string]: any;
|
||||
};
|
||||
export type StreamResponseReturnType = {
|
||||
responseContent: string;
|
||||
totalTokens: number;
|
||||
finishMessages: ChatItemSimpleType[];
|
||||
};
|
||||
|
||||
export const modelServiceToolMap: Record<
|
||||
ChatModelType,
|
||||
{
|
||||
chatCompletion: (data: ChatCompletionType) => Promise<ChatCompletionResponseType>;
|
||||
streamResponse: (data: StreamResponseType) => Promise<StreamResponseReturnType>;
|
||||
}
|
||||
> = {
|
||||
[OpenAiChatEnum.GPT35]: {
|
||||
chatCompletion: (data: ChatCompletionType) =>
|
||||
chatResponse({ model: OpenAiChatEnum.GPT35, ...data }),
|
||||
streamResponse: (data: StreamResponseType) =>
|
||||
openAiStreamResponse({
|
||||
model: OpenAiChatEnum.GPT35,
|
||||
...data
|
||||
})
|
||||
},
|
||||
[OpenAiChatEnum.GPT4]: {
|
||||
chatCompletion: (data: ChatCompletionType) =>
|
||||
chatResponse({ model: OpenAiChatEnum.GPT4, ...data }),
|
||||
streamResponse: (data: StreamResponseType) =>
|
||||
openAiStreamResponse({
|
||||
model: OpenAiChatEnum.GPT4,
|
||||
...data
|
||||
})
|
||||
},
|
||||
[OpenAiChatEnum.GPT432k]: {
|
||||
chatCompletion: (data: ChatCompletionType) =>
|
||||
chatResponse({ model: OpenAiChatEnum.GPT432k, ...data }),
|
||||
streamResponse: (data: StreamResponseType) =>
|
||||
openAiStreamResponse({
|
||||
model: OpenAiChatEnum.GPT432k,
|
||||
...data
|
||||
})
|
||||
},
|
||||
[ClaudeEnum.Claude]: {
|
||||
chatCompletion: claudChat,
|
||||
streamResponse: claudStreamResponse
|
||||
}
|
||||
};
|
||||
|
||||
/* delete invalid symbol */
|
||||
const simplifyStr = (str: string) =>
|
||||
str
|
||||
.replace(/\n+/g, '\n') // 连续空行
|
||||
.replace(/[^\S\r\n]+/g, ' ') // 连续空白内容
|
||||
.trim();
|
||||
|
||||
/* 聊天上下文 tokens 截断 */
|
||||
export const ChatContextFilter = ({
|
||||
model,
|
||||
prompts,
|
||||
maxTokens
|
||||
}: {
|
||||
model: ChatModelType;
|
||||
prompts: ChatItemSimpleType[];
|
||||
maxTokens: number;
|
||||
}) => {
|
||||
const systemPrompts: ChatItemSimpleType[] = [];
|
||||
const chatPrompts: ChatItemSimpleType[] = [];
|
||||
|
||||
let rawTextLen = 0;
|
||||
prompts.forEach((item) => {
|
||||
const val = simplifyStr(item.value);
|
||||
rawTextLen += val.length;
|
||||
|
||||
const data = {
|
||||
obj: item.obj,
|
||||
value: val
|
||||
};
|
||||
|
||||
if (item.obj === ChatRoleEnum.System) {
|
||||
systemPrompts.push(data);
|
||||
} else {
|
||||
chatPrompts.push(data);
|
||||
}
|
||||
});
|
||||
|
||||
// 长度太小时,不需要进行 token 截断
|
||||
if (rawTextLen < maxTokens * 0.5) {
|
||||
return [...systemPrompts, ...chatPrompts];
|
||||
}
|
||||
|
||||
// 去掉 system 的 token
|
||||
maxTokens -= modelToolMap[model].countTokens({
|
||||
messages: systemPrompts
|
||||
});
|
||||
|
||||
// 根据 tokens 截断内容
|
||||
const chats: ChatItemSimpleType[] = [];
|
||||
|
||||
// 从后往前截取对话内容
|
||||
for (let i = chatPrompts.length - 1; i >= 0; i--) {
|
||||
chats.unshift(chatPrompts[i]);
|
||||
|
||||
const tokens = modelToolMap[model].countTokens({
|
||||
messages: chats
|
||||
});
|
||||
|
||||
/* 整体 tokens 超出范围, system必须保留 */
|
||||
if (tokens >= maxTokens) {
|
||||
chats.shift();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return [...systemPrompts, ...chats];
|
||||
};
|
||||
|
||||
/* stream response */
|
||||
export const resStreamResponse = async ({
|
||||
model,
|
||||
res,
|
||||
chatResponse,
|
||||
prompts
|
||||
}: StreamResponseType & {
|
||||
model: ChatModelType;
|
||||
}) => {
|
||||
// 创建响应流
|
||||
res.setHeader('Content-Type', 'text/event-stream;charset-utf-8');
|
||||
res.setHeader('Access-Control-Allow-Origin', '*');
|
||||
res.setHeader('X-Accel-Buffering', 'no');
|
||||
res.setHeader('Cache-Control', 'no-cache, no-transform');
|
||||
|
||||
const { responseContent, totalTokens, finishMessages } = await modelServiceToolMap[
|
||||
model
|
||||
].streamResponse({
|
||||
chatResponse,
|
||||
prompts,
|
||||
res
|
||||
});
|
||||
|
||||
return { responseContent, totalTokens, finishMessages };
|
||||
};
|
120
client/src/service/utils/chat/openai.ts
Normal file
120
client/src/service/utils/chat/openai.ts
Normal file
@@ -0,0 +1,120 @@
|
||||
import { Configuration, OpenAIApi } from 'openai';
|
||||
import { createParser, ParsedEvent, ReconnectInterval } from 'eventsource-parser';
|
||||
import { axiosConfig } from '../tools';
|
||||
import { ChatModelMap, OpenAiChatEnum } from '@/constants/model';
|
||||
import { adaptChatItem_openAI } from '@/utils/plugin/openai';
|
||||
import { modelToolMap } from '@/utils/plugin';
|
||||
import { ChatCompletionType, ChatContextFilter, StreamResponseType } from './index';
|
||||
import { ChatRoleEnum } from '@/constants/chat';
|
||||
|
||||
export const getOpenAIApi = () =>
|
||||
new OpenAIApi(
|
||||
new Configuration({
|
||||
basePath: process.env.OPENAI_BASE_URL || 'https://api.openai.com/v1'
|
||||
})
|
||||
);
|
||||
|
||||
/* 模型对话 */
|
||||
export const chatResponse = async ({
|
||||
model,
|
||||
apiKey,
|
||||
temperature,
|
||||
messages,
|
||||
stream
|
||||
}: ChatCompletionType & { model: `${OpenAiChatEnum}` }) => {
|
||||
const filterMessages = ChatContextFilter({
|
||||
model,
|
||||
prompts: messages,
|
||||
maxTokens: Math.ceil(ChatModelMap[model].contextMaxToken * 0.85)
|
||||
});
|
||||
|
||||
const adaptMessages = adaptChatItem_openAI({ messages: filterMessages });
|
||||
const chatAPI = getOpenAIApi();
|
||||
|
||||
const response = await chatAPI.createChatCompletion(
|
||||
{
|
||||
model,
|
||||
temperature: Number(temperature) || 0,
|
||||
messages: adaptMessages,
|
||||
frequency_penalty: 0.5, // 越大,重复内容越少
|
||||
presence_penalty: -0.5, // 越大,越容易出现新内容
|
||||
stream,
|
||||
stop: ['.!?。']
|
||||
},
|
||||
{
|
||||
timeout: stream ? 60000 : 240000,
|
||||
responseType: stream ? 'stream' : 'json',
|
||||
...axiosConfig(apiKey)
|
||||
}
|
||||
);
|
||||
|
||||
const responseText = stream ? '' : response.data.choices[0].message?.content || '';
|
||||
const totalTokens = stream ? 0 : response.data.usage?.total_tokens || 0;
|
||||
|
||||
return {
|
||||
streamResponse: response,
|
||||
responseMessages: filterMessages.concat({ obj: 'AI', value: responseText }),
|
||||
responseText,
|
||||
totalTokens
|
||||
};
|
||||
};
|
||||
|
||||
/* openai stream response */
|
||||
export const openAiStreamResponse = async ({
|
||||
res,
|
||||
model,
|
||||
chatResponse,
|
||||
prompts
|
||||
}: StreamResponseType & {
|
||||
model: `${OpenAiChatEnum}`;
|
||||
}) => {
|
||||
try {
|
||||
let responseContent = '';
|
||||
|
||||
const onParse = async (event: ParsedEvent | ReconnectInterval) => {
|
||||
if (event.type !== 'event') return;
|
||||
const data = event.data;
|
||||
if (data === '[DONE]') return;
|
||||
try {
|
||||
const json = JSON.parse(data);
|
||||
const content: string = json?.choices?.[0].delta.content || '';
|
||||
responseContent += content;
|
||||
|
||||
!res.closed && content && res.write(content);
|
||||
} catch (error) {
|
||||
error;
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
const decoder = new TextDecoder();
|
||||
const parser = createParser(onParse);
|
||||
for await (const chunk of chatResponse.data as any) {
|
||||
if (res.closed) {
|
||||
break;
|
||||
}
|
||||
parser.feed(decoder.decode(chunk, { stream: true }));
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('pipe error', error);
|
||||
}
|
||||
|
||||
// count tokens
|
||||
const finishMessages = prompts.concat({
|
||||
obj: ChatRoleEnum.AI,
|
||||
value: responseContent
|
||||
});
|
||||
|
||||
const totalTokens = modelToolMap[model].countTokens({
|
||||
messages: finishMessages
|
||||
});
|
||||
|
||||
return {
|
||||
responseContent,
|
||||
totalTokens,
|
||||
finishMessages
|
||||
};
|
||||
} catch (error) {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
};
|
36
client/src/service/utils/promotion.ts
Normal file
36
client/src/service/utils/promotion.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { promotionRecord } from '../mongo';
|
||||
|
||||
export const pushPromotionRecord = async ({
|
||||
userId,
|
||||
objUId,
|
||||
type,
|
||||
amount
|
||||
}: {
|
||||
userId: string;
|
||||
objUId: string;
|
||||
type: 'invite' | 'shareModel';
|
||||
amount: number;
|
||||
}) => {
|
||||
try {
|
||||
await promotionRecord.create({
|
||||
userId,
|
||||
objUId,
|
||||
type,
|
||||
amount
|
||||
});
|
||||
} catch (error) {
|
||||
console.log('创建推广记录异常', error);
|
||||
}
|
||||
};
|
||||
|
||||
export const withdrawRecord = async ({ userId, amount }: { userId: string; amount: number }) => {
|
||||
try {
|
||||
await promotionRecord.create({
|
||||
userId,
|
||||
type: 'withdraw',
|
||||
amount
|
||||
});
|
||||
} catch (error) {
|
||||
console.log('提现记录异常', error);
|
||||
}
|
||||
};
|
71
client/src/service/utils/sendNote.ts
Normal file
71
client/src/service/utils/sendNote.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
import * as nodemailer from 'nodemailer';
|
||||
import { UserAuthTypeEnum } from '@/constants/common';
|
||||
import Dysmsapi, * as dysmsapi from '@alicloud/dysmsapi20170525';
|
||||
// @ts-ignore
|
||||
import * as OpenApi from '@alicloud/openapi-client';
|
||||
// @ts-ignore
|
||||
import * as Util from '@alicloud/tea-util';
|
||||
|
||||
const myEmail = process.env.MY_MAIL;
|
||||
const mailTransport = nodemailer.createTransport({
|
||||
// host: 'smtp.qq.phone',
|
||||
service: 'qq',
|
||||
secure: true, //安全方式发送,建议都加上
|
||||
auth: {
|
||||
user: myEmail,
|
||||
pass: process.env.MAILE_CODE
|
||||
}
|
||||
});
|
||||
|
||||
const emailMap: { [key: string]: any } = {
|
||||
[UserAuthTypeEnum.register]: {
|
||||
subject: '注册 FastGPT 账号',
|
||||
html: (code: string) => `<div>您正在注册 FastGPT 账号,验证码为:${code}</div>`
|
||||
},
|
||||
[UserAuthTypeEnum.findPassword]: {
|
||||
subject: '修改 FastGPT 密码',
|
||||
html: (code: string) => `<div>您正在修改 FastGPT 账号密码,验证码为:${code}</div>`
|
||||
}
|
||||
};
|
||||
|
||||
export const sendEmailCode = (email: string, code: string, type: `${UserAuthTypeEnum}`) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const options = {
|
||||
from: `"FastGPT" ${myEmail}`,
|
||||
to: email,
|
||||
subject: emailMap[type]?.subject,
|
||||
html: emailMap[type]?.html(code)
|
||||
};
|
||||
mailTransport.sendMail(options, function (err, msg) {
|
||||
if (err) {
|
||||
console.log('send email error->', err);
|
||||
reject('发生邮件异常');
|
||||
} else {
|
||||
resolve('');
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
export const sendPhoneCode = async (phone: string, code: string) => {
|
||||
const accessKeyId = process.env.aliAccessKeyId;
|
||||
const accessKeySecret = process.env.aliAccessKeySecret;
|
||||
const signName = process.env.aliSignName;
|
||||
const templateCode = process.env.aliTemplateCode;
|
||||
const endpoint = 'dysmsapi.aliyuncs.com';
|
||||
|
||||
const sendSmsRequest = new dysmsapi.SendSmsRequest({
|
||||
phoneNumbers: phone,
|
||||
signName,
|
||||
templateCode,
|
||||
templateParam: `{"code":${code}}`
|
||||
});
|
||||
|
||||
const config = new OpenApi.Config({ accessKeyId, accessKeySecret, endpoint });
|
||||
const client = new Dysmsapi(config);
|
||||
const runtime = new Util.RuntimeOptions({});
|
||||
const res = await client.sendSmsWithOptions(sendSmsRequest, runtime);
|
||||
if (res.body.code !== 'OK') {
|
||||
return Promise.reject(res.body.message || '发送短信失败');
|
||||
}
|
||||
};
|
72
client/src/service/utils/tools.ts
Normal file
72
client/src/service/utils/tools.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
import type { NextApiResponse, NextApiHandler, NextApiRequest } from 'next';
|
||||
import NextCors from 'nextjs-cors';
|
||||
import crypto from 'crypto';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import { generateQA } from '../events/generateQA';
|
||||
import { generateVector } from '../events/generateVector';
|
||||
|
||||
/* 密码加密 */
|
||||
export const hashPassword = (psw: string) => {
|
||||
return crypto.createHash('sha256').update(psw).digest('hex');
|
||||
};
|
||||
|
||||
/* 生成 token */
|
||||
export const generateToken = (userId: string) => {
|
||||
const key = process.env.TOKEN_KEY as string;
|
||||
const token = jwt.sign(
|
||||
{
|
||||
userId,
|
||||
exp: Math.floor(Date.now() / 1000) + 60 * 60 * 24 * 7
|
||||
},
|
||||
key
|
||||
);
|
||||
return token;
|
||||
};
|
||||
|
||||
/* set cookie */
|
||||
export const setCookie = (res: NextApiResponse, userId: string) => {
|
||||
res.setHeader('Set-Cookie', `token=${generateToken(userId)}; Path=/; HttpOnly; Max-Age=604800`);
|
||||
};
|
||||
/* clear cookie */
|
||||
export const clearCookie = (res: NextApiResponse) => {
|
||||
res.setHeader('Set-Cookie', 'token=; Path=/; Max-Age=0');
|
||||
};
|
||||
|
||||
/* openai axios config */
|
||||
export const axiosConfig = (apikey: string) => ({
|
||||
baseURL: process.env.OPENAI_BASE_URL || 'https://api.openai.com/v1',
|
||||
httpsAgent: global.httpsAgent,
|
||||
headers: {
|
||||
Authorization: `Bearer ${apikey}`,
|
||||
auth: process.env.OPENAI_BASE_URL_AUTH || ''
|
||||
}
|
||||
});
|
||||
|
||||
export function withNextCors(handler: NextApiHandler): NextApiHandler {
|
||||
return async function nextApiHandlerWrappedWithNextCors(
|
||||
req: NextApiRequest,
|
||||
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);
|
||||
};
|
||||
}
|
||||
|
||||
export const startQueue = () => {
|
||||
const qaMax = Number(process.env.QA_MAX_PROCESS || 10);
|
||||
const vectorMax = Number(process.env.VECTOR_MAX_PROCESS || 10);
|
||||
|
||||
for (let i = 0; i < qaMax; i++) {
|
||||
generateQA();
|
||||
}
|
||||
for (let i = 0; i < vectorMax; i++) {
|
||||
generateVector();
|
||||
}
|
||||
};
|
31
client/src/service/utils/wxpay.ts
Normal file
31
client/src/service/utils/wxpay.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
// @ts-ignore
|
||||
import Payment from 'wxpay-v3';
|
||||
|
||||
export const getPayment = () => {
|
||||
return new Payment({
|
||||
appid: process.env.WX_APPID,
|
||||
mchid: process.env.WX_MCHID,
|
||||
private_key: process.env.WX_PRIVATE_KEY?.replace(/\\n/g, '\n'),
|
||||
serial_no: process.env.WX_SERIAL_NO,
|
||||
apiv3_private_key: process.env.WX_V3_CODE,
|
||||
notify_url: process.env.WX_NOTIFY_URL
|
||||
});
|
||||
};
|
||||
|
||||
export const nativePay = (amount: number, payId: string): Promise<string> =>
|
||||
getPayment()
|
||||
.native({
|
||||
description: 'Fast GPT 余额充值',
|
||||
out_trade_no: payId,
|
||||
amount: {
|
||||
total: amount
|
||||
}
|
||||
})
|
||||
.then((res: any) => JSON.parse(res.data).code_url);
|
||||
|
||||
export const getPayResult = (payId: string) =>
|
||||
getPayment()
|
||||
.getTransactionsByOutTradeNo({
|
||||
out_trade_no: payId
|
||||
})
|
||||
.then((res: any) => JSON.parse(res.data));
|
Reference in New Issue
Block a user