Commit ba9d9c3d by archer

new framwork

parent d9450bd7
# 运行端口,如果不是 3000 口运行,需要改成其他的。注意:不是改了这个变量就会变成其他端口,而是因为改成其他端口,才用这个变量。
PORT=3000
# 代理
# AXIOS_PROXY_HOST=127.0.0.1
# AXIOS_PROXY_PORT=7890
# email
MY_MAIL=xxx@qq.com
MAILE_CODE=xxx
# ali ems
aliAccessKeyId=xxx
aliAccessKeySecret=xxx
aliSignName=xxx
aliTemplateCode=SMS_xxx
# token
TOKEN_KEY=xxx
# root key, 最高权限
ROOT_KEY=xxx
# 是否进行安全校验(1: 开启,0: 关闭)
SENSITIVE_CHECK=1
# openai
# OPENAI_BASE_URL=https://api.openai.com/v1
# OPENAI_BASE_URL_AUTH=可选的安全凭证(不需要的时候,记得去掉)
OPENAIKEY=sk-xxx # 对话用的key
OPENAI_TRAINING_KEY=sk-xxx # 训练用的key
GPT4KEY=sk-xxx
# claude
CLAUDE_BASE_URL=calude模型请求地址
CLAUDE_KEY=CLAUDE_KEY
# db
MONGODB_URI=mongodb://username:password@0.0.0.0:27017/test?authSource=admin
PG_HOST=0.0.0.0
PG_PORT=8100
PG_USER=xxx
PG_PASSWORD=xxx
PG_DB_NAME=xxx
\ No newline at end of file
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
node_modules/
# next.js
/.next/
/out/
.next/
out/
# production
/build
build/
# misc
.DS_Store
......@@ -34,6 +25,7 @@ yarn-error.log*
# typescript
*.tsbuildinfo
next-env.d.ts
/.vscode/
platform.json
testApi/
\ No newline at end of file
testApi/
local/
.husky/
\ No newline at end of file
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
if command -v npx >/dev/null 2>&1; then
npx lint-staged
fi
\ No newline at end of file
{
"editor.formatOnSave": true, //每次保存自动格式化
"editor.mouseWheelZoom": true,
"typescript.tsdk": "./client/node_modules/typescript/lib",
"prettier.prettierPath": "./node_modules/prettier"
}
\ No newline at end of file
SERVICE_NAME=fastgpt
# Image URL to use all building/pushing image targets
IMG ?= $(SERVICE_NAME):latest
.PHONY: all
all: build
##@ General
# The help target prints out all targets with their descriptions organized
# beneath their categories. The categories are represented by '##@' and the
# target descriptions by '##'. The awk commands is responsible for reading the
# entire set of makefiles included in this invocation, looking for lines of the
# file as xyz: ## something, and then pretty-format the target and help. Then,
# if there's a line with ##@ something, that gets pretty-printed as a category.
# More info on the usage of ANSI control characters for terminal formatting:
# https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_parameters
# More info on the awk command:
# http://linuxcommand.org/lc3_adv_awk.php
.PHONY: help
help: ## Display this help.
@awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m<target>\033[0m\n"} /^[a-zA-Z_0-9-]+:.*?##/ { printf " \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST)
##@ Build
.PHONY: build
build: ## Build desktop-frontend binary.
pnpm run build
.PHONY: run
run: ## Run a dev service from host.
pnpm run start
.PHONY: docker-build
docker-build: ## Build docker image with the desktop-frontend.
docker build -t registry.cn-hangzhou.aliyuncs.com/fastgpt/fastgpt:latest . --network host --build-arg HTTP_PROXY=http://127.0.0.1:7890 --build-arg HTTPS_PROXY=http://127.0.0.1:7890
##@ Deployment
.PHONY: docker-run
docker-run: ## Push docker image.
docker run -d -p 8008:3000 --name fastgpt -v /web_project/yjl/fastgpt/logs:/app/.next/logs registry.cn-hangzhou.aliyuncs.com/fastgpt/fastgpt:latest
#TODO: add support of docker push
#TODO: add support of sealos apply
# dependencies
node_modules/
# next.js
.next/
out/
# production
build/
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
# local env files
.env*.local
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts
platform.json
testApi/
local/
.husky/
\ No newline at end of file
{
"name": "fastgpt",
"version": "3.7",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"@alicloud/dysmsapi20170525": "^2.0.23",
"@alicloud/openapi-client": "^0.4.5",
"@alicloud/tea-util": "^1.4.5",
"@chakra-ui/icons": "^2.0.17",
"@chakra-ui/react": "^2.5.1",
"@chakra-ui/system": "^2.5.5",
"@dqbd/tiktoken": "^1.0.6",
"@emotion/react": "^11.10.6",
"@emotion/styled": "^11.10.6",
"@next/font": "13.1.6",
"@tanstack/react-query": "^4.24.10",
"@types/nprogress": "^0.2.0",
"axios": "^1.3.3",
"cookie": "^0.5.0",
"crypto": "^1.0.1",
"dayjs": "^1.11.7",
"eventsource-parser": "^0.1.0",
"formidable": "^2.1.1",
"framer-motion": "^9.0.6",
"graphemer": "^1.4.0",
"hyperdown": "^2.4.29",
"immer": "^9.0.19",
"jsonwebtoken": "^9.0.0",
"lodash": "^4.17.21",
"mammoth": "^1.5.1",
"mongoose": "^6.10.0",
"nanoid": "^4.0.1",
"next": "13.1.6",
"nextjs-cors": "^2.1.2",
"nodemailer": "^6.9.1",
"nprogress": "^0.2.0",
"openai": "^3.2.1",
"papaparse": "^5.4.1",
"pg": "^8.10.0",
"react": "18.2.0",
"react-dom": "18.2.0",
"react-hook-form": "^7.43.1",
"react-markdown": "^8.0.5",
"react-syntax-highlighter": "^15.5.0",
"rehype-katex": "^6.0.2",
"remark-gfm": "^3.0.1",
"remark-math": "^5.1.1",
"request-ip": "^3.3.0",
"sass": "^1.58.3",
"tunnel": "^0.0.6",
"wxpay-v3": "^3.0.2",
"zustand": "^4.3.5"
},
"devDependencies": {
"@svgr/webpack": "^6.5.1",
"@types/cookie": "^0.5.1",
"@types/formidable": "^2.0.5",
"@types/jsonwebtoken": "^9.0.1",
"@types/lodash": "^4.14.191",
"@types/node": "18.14.0",
"@types/nodemailer": "^6.4.7",
"@types/papaparse": "^5.3.7",
"@types/pg": "^8.6.6",
"@types/react": "18.0.28",
"@types/react-dom": "18.0.11",
"@types/react-syntax-highlighter": "^15.5.6",
"@types/request-ip": "^0.0.37",
"@types/tunnel": "^0.0.3",
"eslint": "8.34.0",
"eslint-config-next": "13.1.6",
"typescript": "4.9.5"
},
"engines": {
"node": ">=18.0.0"
}
}
This source diff could not be displayed because it is too large. You can view the blob instead.
......@@ -7,58 +7,61 @@ interface StreamFetchProps {
abortSignal: AbortController;
}
export const streamFetch = ({ url, data, onMessage, abortSignal }: StreamFetchProps) =>
new Promise<{ responseText: string; newChatId: string; systemPrompt: string; quoteLen: number }>(
async (resolve, reject) => {
try {
const res = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(data),
signal: abortSignal.signal
});
const reader = res.body?.getReader();
if (!reader) return;
new Promise<{
responseText: string;
newChatId: string;
systemPrompt: string;
quoteLen: number;
}>(async (resolve, reject) => {
try {
const res = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(data),
signal: abortSignal.signal
});
const reader = res.body?.getReader();
if (!reader) return;
const decoder = new TextDecoder();
const decoder = new TextDecoder();
const newChatId = decodeURIComponent(res.headers.get(NEW_CHATID_HEADER) || '');
const systemPrompt = decodeURIComponent(res.headers.get(GUIDE_PROMPT_HEADER) || '').trim();
const quoteLen = res.headers.get(QUOTE_LEN_HEADER)
? Number(res.headers.get(QUOTE_LEN_HEADER))
: 0;
const newChatId = decodeURIComponent(res.headers.get(NEW_CHATID_HEADER) || '');
const systemPrompt = decodeURIComponent(res.headers.get(GUIDE_PROMPT_HEADER) || '').trim();
const quoteLen = res.headers.get(QUOTE_LEN_HEADER)
? Number(res.headers.get(QUOTE_LEN_HEADER))
: 0;
let responseText = '';
let responseText = '';
const read = async () => {
try {
const { done, value } = await reader?.read();
if (done) {
if (res.status === 200) {
resolve({ responseText, newChatId, quoteLen, systemPrompt });
} else {
const parseError = JSON.parse(responseText);
reject(parseError?.message || '请求异常');
}
return;
}
const text = decoder.decode(value);
responseText += text;
onMessage(text);
read();
} catch (err: any) {
if (err?.message === 'The user aborted a request.') {
return resolve({ responseText, newChatId, quoteLen, systemPrompt });
const read = async () => {
try {
const { done, value } = await reader?.read();
if (done) {
if (res.status === 200) {
resolve({ responseText, newChatId, quoteLen, systemPrompt });
} else {
const parseError = JSON.parse(responseText);
reject(parseError?.message || '请求异常');
}
reject(typeof err === 'string' ? err : err?.message || '请求异常');
return;
}
const text = decoder.decode(value);
responseText += text;
onMessage(text);
read();
} catch (err: any) {
if (err?.message === 'The user aborted a request.') {
return resolve({ responseText, newChatId, quoteLen, systemPrompt });
}
};
read();
} catch (err: any) {
console.log(err, '====');
reject(typeof err === 'string' ? err : err?.message || '请求异常');
}
reject(typeof err === 'string' ? err : err?.message || '请求异常');
}
};
read();
} catch (err: any) {
console.log(err, '====');
reject(typeof err === 'string' ? err : err?.message || '请求异常');
}
);
});
......@@ -7,7 +7,12 @@ import {
Response as PushDateResponse
} from '@/pages/api/openapi/kb/pushData';
export type KbUpdateParams = { id: string; name: string; tags: string; avatar: string };
export type KbUpdateParams = {
id: string;
name: string;
tags: string;
avatar: string;
};
/* knowledge base */
export const getKbList = () => GET<KbItemType[]>(`/plugins/kb/list`);
......
......@@ -5,7 +5,10 @@ import { authUser } from '@/service/utils/auth';
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try {
const { chatId, contentId } = req.query as { chatId: string; contentId: string };
const { chatId, contentId } = req.query as {
chatId: string;
contentId: string;
};
if (!chatId || !contentId) {
throw new Error('缺少参数');
......
......@@ -6,7 +6,10 @@ import { Types } from 'mongoose';
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try {
const { chatId, historyId } = req.query as { chatId: string; historyId: string };
const { chatId, historyId } = req.query as {
chatId: string;
historyId: string;
};
await connectToDatabase();
const { userId } = await authUser({ req, authToken: true });
......
......@@ -14,7 +14,10 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
try {
const { userId } = await authUser({ req, authToken: true });
let { modelId, chatId } = req.query as { modelId: '' | string; chatId: '' | string };
let { modelId, chatId } = req.query as {
modelId: '' | string;
chatId: '' | string;
};
await connectToDatabase();
......@@ -36,7 +39,12 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
modelId = model._id;
} else {
// 校验使用权限
const authRes = await authModel({ modelId, userId, authUser: false, authOwner: false });
const authRes = await authModel({
modelId,
userId,
authUser: false,
authOwner: false
});
model = authRes.model;
}
......
......@@ -30,7 +30,9 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
const where = {
$and: [
{ 'share.isShare': true },
{ $or: [{ name: { $regex: regex } }, { 'share.intro': { $regex: regex } }] }
{
$or: [{ name: { $regex: regex } }, { 'share.intro': { $regex: regex } }]
}
]
};
const pipeline = [
......@@ -66,7 +68,11 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
userId: 1,
share: 1,
isCollection: {
$cond: { if: { $gt: [{ $size: '$collections' }, 0] }, then: true, else: false }
$cond: {
if: { $gt: [{ $size: '$collections' }, 0] },
then: true,
else: false
}
}
}
},
......
......@@ -12,7 +12,12 @@ import { ChatRoleEnum } from '@/constants/chat';
import { openaiEmbedding } from '../plugin/openaiEmbedding';
import { modelToolMap } from '@/utils/plugin';
export type QuoteItemType = { id: string; q: string; a: string; source?: string };
export type QuoteItemType = {
id: string;
q: string;
a: string;
source?: string;
};
type Props = {
prompts: ChatItemSimpleType[];
similarity: number;
......
......@@ -7,7 +7,10 @@ import { adaptBill } from '@/utils/adapt';
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try {
let { pageNum = 1, pageSize = 10 } = req.query as { pageNum: string; pageSize: string };
let { pageNum = 1, pageSize = 10 } = req.query as {
pageNum: string;
pageSize: string;
};
pageNum = +pageNum;
pageSize = +pageSize;
......
......@@ -17,7 +17,12 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
// 计算累计合
const countHistory: { totalAmount: number }[] = await promotionRecord.aggregate([
{ $match: { userId: new mongoose.Types.ObjectId(userId), amount: { $gt: 0 } } },
{
$match: {
userId: new mongoose.Types.ObjectId(userId),
amount: { $gt: 0 }
}
},
{
$group: {
_id: null, // 分组条件,这里使用 null 表示不分组
......
......@@ -6,7 +6,10 @@ import { authUser } from '@/service/utils/auth';
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try {
let { pageNum = 1, pageSize = 10 } = req.query as { pageNum: string; pageSize: string };
let { pageNum = 1, pageSize = 10 } = req.query as {
pageNum: string;
pageSize: string;
};
pageNum = +pageNum;
pageSize = +pageSize;
......
......@@ -36,7 +36,10 @@ const PcSliderBar = ({
}) => {
const router = useRouter();
const { toast } = useToast();
const { modelId = '', chatId = '' } = router.query as { modelId: string; chatId: string };
const { modelId = '', chatId = '' } = router.query as {
modelId: string;
chatId: string;
};
const ContextMenuRef = useRef(null);
const theme = useTheme();
......
......@@ -31,7 +31,10 @@ const PcSliderBar = ({
onCloseSlider: () => void;
}) => {
const router = useRouter();
const { shareId = '', historyId = '' } = router.query as { shareId: string; historyId: string };
const { shareId = '', historyId = '' } = router.query as {
shareId: string;
historyId: string;
};
const theme = useTheme();
const { isPc } = useGlobalStore();
......
......@@ -54,7 +54,10 @@ const SelectFileModal = ({
const [btnLoading, setBtnLoading] = useState(false);
const { toast } = useToast();
const [prompt, setPrompt] = useState('');
const { File, onOpen } = useSelectFile({ fileType: fileExtension, multiple: true });
const { File, onOpen } = useSelectFile({
fileType: fileExtension,
multiple: true
});
const [mode, setMode] = useState<`${TrainingModeEnum}`>(TrainingModeEnum.index);
const [files, setFiles] = useState<{ filename: string; text: string }[]>([
{ filename: '文本1', text: '' }
......@@ -224,7 +227,8 @@ const SelectFileModal = ({
>
<Box mt={2} px={5} maxW={['100%', '70%']} textAlign={'justify'} color={'blackAlpha.600'}>
支持 {fileExtension} 文件。Gpt会自动对文本进行 QA 拆分,需要较长训练时间,拆分需要消耗
tokens,账号余额不足时,未拆分的数据会被删除。一个{files.length}个文本。
tokens,账号余额不足时,未拆分的数据会被删除。一个{files.length}
个文本。
</Box>
{/* 拆分模式 */}
<Flex w={'100%'} px={5} alignItems={'center'} mt={4}>
......
......@@ -24,7 +24,9 @@ const ModelList = ({ modelId }: { modelId: string }) => {
const onclickCreateModel = useCallback(async () => {
setIsLoading(true);
try {
const id = await postCreateModel({ name: `AI应用${myModels.length + 1}` });
const id = await postCreateModel({
name: `AI应用${myModels.length + 1}`
});
toast({
title: '创建成功',
status: 'success'
......
......@@ -341,7 +341,9 @@ ${e.password ? `密码为: ${e.password}` : ''}`;
</Box>
<Select
isDisabled={!isOwner}
{...register('chat.searchMode', { required: '搜索模式不能为空' })}
{...register('chat.searchMode', {
required: '搜索模式不能为空'
})}
>
{Object.entries(ModelVectorSearchModeMap).map(([key, { text }]) => (
<option key={key} value={key}>
......
import { Schema, model, models, Model } from 'mongoose';
import { hashPassword } from '@/service/utils/tools';
import { PRICE_SCALE } from '@/constants/common';
import { UserModelSchema } from '@/types/mongoSchema';
const UserSchema = new Schema({
username: {
// 可以是手机/邮箱,新的验证都只用手机
type: String,
required: true,
unique: true // 唯一
},
password: {
type: String,
required: true,
set: (val: string) => hashPassword(val),
get: (val: string) => hashPassword(val),
select: false
},
createTime: {
type: Date,
default: () => new Date()
},
avatar: {
type: String,
default: '/icon/human.png'
},
balance: {
// 平台余额,不可提现
type: Number,
default: 2 * PRICE_SCALE
},
inviterId: {
// 谁邀请注册的
type: Schema.Types.ObjectId,
ref: 'user'
},
promotion: {
rate: {
// 返现比例
type: Number,
default: 15
}
},
openaiKey: {
type: String,
default: ''
},
limit: {
exportKbTime: {
// Every half hour
type: Date
}
}
});
export const User: Model<UserModelSchema> = models['user'] || model('user', UserSchema);
import mongoose from 'mongoose';
import tunnel from 'tunnel';
import { TrainingData } from './mongo';
import { startQueue } from './utils/tools';
/**
* 连接 MongoDB 数据库
*/
export async function connectToDatabase(): Promise<void> {
if (global.mongodb) {
return;
}
global.mongodb = 'connecting';
try {
mongoose.set('strictQuery', true);
global.mongodb = await mongoose.connect(process.env.MONGODB_URI as string, {
bufferCommands: true,
dbName: process.env.MONGODB_NAME,
maxPoolSize: 5,
minPoolSize: 1,
maxConnecting: 5
});
console.log('mongo connected');
} catch (error) {
console.log('error->', 'mongo connect error');
global.mongodb = null;
}
// 创建代理对象
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
}
});
}
// 初始化队列
global.qaQueueLen = 0;
global.vectorQueueLen = 0;
startQueue();
}
export * from './models/authCode';
export * from './models/chat';
export * from './models/model';
export * from './models/user';
export * from './models/bill';
export * from './models/pay';
export * from './models/trainingData';
export * from './models/openapi';
export * from './models/promotionRecord';
export * from './models/collection';
export * from './models/shareChat';
export * from './models/kb';
export * from './models/inform';
import { NextApiResponse } from 'next';
import {
openaiError,
openaiAccountError,
proxyError,
ERROR_RESPONSE,
ERROR_ENUM
} from './errorCode';
import { clearCookie } from './utils/tools';
export interface ResponseType<T = any> {
code: number;
message: string;
data: T;
}
export const jsonRes = <T = any>(
res: NextApiResponse,
props?: {
code?: number;
message?: string;
data?: T;
error?: any;
}
) => {
const { code = 200, message = '', data = null, error } = props || {};
const errResponseKey = typeof error === 'string' ? error : error?.message;
// Specified error
if (ERROR_RESPONSE[errResponseKey]) {
// login is expired
if (errResponseKey === ERROR_ENUM.unAuthorization) {
clearCookie(res);
}
return res.json(ERROR_RESPONSE[errResponseKey]);
}
// another error
let msg = message || error?.message;
if ((code < 200 || code >= 400) && !message) {
msg = error?.message || '请求错误';
if (typeof error === 'string') {
msg = error;
} else if (proxyError[error?.code]) {
msg = '接口连接异常';
} else if (error?.response?.data?.error?.message) {
msg = error?.response?.data?.error?.message;
} else if (openaiAccountError[error?.response?.data?.error?.code]) {
msg = openaiAccountError[error?.response?.data?.error?.code];
} else if (openaiError[error?.response?.statusText]) {
msg = openaiError[error.response.statusText];
}
console.log(error);
}
res.json({
code,
statusText: '',
message: msg,
data: data !== undefined ? data : null
});
};
......@@ -227,7 +227,10 @@ export const authModel = async ({
};
}
return { model, showModelDetail: model.share.isShareDetail || userId === String(model.userId) };
return {
model,
showModelDetail: model.share.isShareDetail || userId === String(model.userId)
};
};
// 知识库操作权限
......@@ -287,7 +290,10 @@ export const authChat = async ({
]);
}
// 获取 user 的 apiKey
const { userOpenAiKey, systemAuthKey } = await getApiKey({ model: model.chat.chatModel, userId });
const { userOpenAiKey, systemAuthKey } = await getApiKey({
model: model.chat.chatModel,
userId
});
return {
userOpenAiKey,
......
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);
}
};
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);
}
};
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 || '发送短信失败');
}
};
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();
}
};
// @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));
import { create } from 'zustand';
import { devtools } from 'zustand/middleware';
import { immer } from 'zustand/middleware/immer';
import type { InitDateResponse } from '@/pages/api/system/getInitData';
import { getInitData } from '@/api/system';
type State = {
initData: InitDateResponse;
loadInitData: () => Promise<void>;
loading: boolean;
setLoading: (val: boolean) => null;
screenWidth: number;
setScreenWidth: (val: number) => void;
isPc: boolean;
};
export const useGlobalStore = create<State>()(
devtools(
immer((set, get) => ({
initData: {
beianText: '',
googleVerKey: ''
},
async loadInitData() {
try {
const res = await getInitData();
set((state) => {
state.initData = res;
});
} catch (error) {}
},
loading: false,
setLoading: (val: boolean) => {
set((state) => {
state.loading = val;
});
return null;
},
screenWidth: 600,
setScreenWidth(val: number) {
set((state) => {
state.screenWidth = val;
state.isPc = val < 900 ? false : true;
});
},
isPc: false
}))
)
);
body,
h1,
h2,
h3,
h4,
hr,
p,
blockquote,
dl,
dt,
dd,
ul,
ol,
li,
pre,
form,
fieldset,
legend,
button,
input,
textarea,
th,
td,
svg {
margin: 0;
}
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: transparent;
border-radius: 2px;
}
::-webkit-scrollbar-thumb {
background: rgba(189, 193, 197, 0.7);
border-radius: 2px;
}
::-webkit-scrollbar-thumb:hover {
background: rgba(189, 193, 197, 1);
}
div {
&::-webkit-scrollbar-thumb {
background: transparent !important;
transition: 1s;
}
&:hover {
&::-webkit-scrollbar-thumb {
background: rgba(189, 193, 197, 0.7) !important;
}
&::-webkit-scrollbar-thumb:hover {
background: rgba(189, 193, 197, 1) !important;
}
}
}
input::placeholder,
textarea::placeholder {
font-size: 0.85em;
}
* {
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
-webkit-focus-ring-color: rgba(0, 0, 0, 0);
outline: none;
}
#__next {
height: 100%;
}
#nprogress .bar {
background: '#85b1ff' !important; //自定义颜色
}
.textEllipsis {
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
}
.grecaptcha-badge {
display: none !important;
}
@media (max-width: 900px) {
html {
font-size: 14px;
}
::-webkit-scrollbar {
width: 2px;
height: 2px;
}
}
@supports (bottom: constant(safe-area-inset-bottom)) or (bottom: env(safe-area-inset-bottom)) {
body {
padding-bottom: constant(safe-area-inset-bottom);
padding-bottom: env(safe-area-inset-bottom);
}
}
import { ChatRoleEnum } from '@/constants/chat';
import type { InitChatResponse, InitShareChatResponse } from '@/api/response/chat';
import { QuoteItemType } from '@/pages/api/openapi/kb/appKbSearch';
export type ExportChatType = 'md' | 'pdf' | 'html';
export type ChatItemSimpleType = {
obj: `${ChatRoleEnum}`;
value: string;
quoteLen?: number;
quote?: QuoteItemType[];
systemPrompt?: string;
};
export type ChatItemType = {
_id: string;
} & ChatItemSimpleType;
export type ChatSiteItemType = {
status: 'loading' | 'finish';
} & ChatItemType;
export interface ChatType extends InitChatResponse {
history: ChatSiteItemType[];
}
export interface ShareChatType extends InitShareChatResponse {
history: ChatSiteItemType[];
}
export type HistoryItemType = {
_id: string;
updateTime: Date;
modelId: string;
title: string;
latestChat: string;
top: boolean;
};
export type ShareChatHistoryItemType = {
_id: string;
shareId: string;
updateTime: Date;
title: string;
latestChat: string;
chats: ChatSiteItemType[];
};
import type { Mongoose } from 'mongoose';
import type { Agent } from 'http';
import type { Pool } from 'pg';
import type { Tiktoken } from '@dqbd/tiktoken';
declare global {
var mongodb: Mongoose | string | null;
var pgClient: Pool | null;
var httpsAgent: Agent;
var particlesJS: any;
var grecaptcha: any;
var QRCode: any;
var qaQueueLen: number;
var vectorQueueLen: number;
var OpenAiEncMap: Record<string, Tiktoken>;
interface Window {
['pdfjs-dist/build/pdf']: any;
}
}
export type PagingData<T> = {
pageNum: number;
pageSize: number;
data: T[];
total?: number;
};
export type RequestPaging = { pageNum: number; pageSize: number; [key]: any };
import { ModelStatusEnum } from '@/constants/model';
import type { ModelSchema, kbSchema } from './mongoSchema';
import { ChatModelType, appVectorSearchModeEnum } from '@/constants/model';
export type ModelListItemType = {
_id: string;
name: string;
avatar: string;
systemPrompt: string;
};
export interface ModelUpdateParams {
name: string;
avatar: string;
chat: ModelSchema['chat'];
share: ModelSchema['share'];
}
export interface ShareModelItem {
_id: string;
avatar: string;
name: string;
userId: string;
share: ModelSchema['share'];
isCollection: boolean;
}
export type ShareChatEditType = {
name: string;
password: string;
maxContext: number;
};
import type { ChatItemType } from './chat';
import {
ModelStatusEnum,
ModelNameEnum,
appVectorSearchModeEnum,
ChatModelType,
EmbeddingModelType
} from '@/constants/model';
import type { DataType } from './data';
import { BillTypeEnum, InformTypeEnum } from '@/constants/user';
import { TrainingModeEnum } from '@/constants/plugin';
export interface UserModelSchema {
_id: string;
username: string;
password: string;
avatar: string;
balance: number;
inviterId?: string;
promotionAmount: number;
openaiKey: string;
createTime: number;
promotion: {
rate: number;
};
limit: {
exportKbTime?: Date;
};
}
export interface AuthCodeSchema {
_id: string;
username: string;
code: string;
type: 'register' | 'findPassword';
expiredTime: number;
}
export interface ModelSchema {
_id: string;
userId: string;
name: string;
avatar: string;
status: `${ModelStatusEnum}`;
updateTime: number;
chat: {
relatedKbs: string[];
searchMode: `${appVectorSearchModeEnum}`;
systemPrompt: string;
temperature: number;
chatModel: ChatModelType; // 聊天时用的模型,训练后就是训练的模型
};
share: {
isShare: boolean;
isShareDetail: boolean;
intro: string;
collection: number;
};
}
export interface ModelPopulate extends ModelSchema {
userId: UserModelSchema;
}
export interface CollectionSchema {
modelId: string;
userId: string;
}
export type ModelDataType = 0 | 1;
export interface TrainingDataSchema {
_id: string;
userId: string;
kbId: string;
lockTime: Date;
mode: `${TrainingModeEnum}`;
prompt: string;
q: string;
a: string;
source: string;
}
export interface ChatSchema {
_id: string;
userId: string;
modelId: string;
expiredTime: number;
updateTime: Date;
title: string;
customTitle: string;
latestChat: string;
top: boolean;
content: ChatItemType[];
}
export interface ChatPopulate extends ChatSchema {
userId: UserModelSchema;
modelId: ModelSchema;
}
export interface BillSchema {
_id: string;
userId: string;
type: `${BillTypeEnum}`;
modelName: ChatModelType | EmbeddingModelType;
chatId: string;
time: Date;
textLen: number;
tokenLen: number;
price: number;
}
export interface PaySchema {
_id: string;
userId: string;
createTime: Date;
price: number;
orderId: string;
status: 'SUCCESS' | 'REFUND' | 'NOTPAY' | 'CLOSED';
}
export interface OpenApiSchema {
_id: string;
userId: string;
createTime: Date;
lastUsedTime?: Date;
apiKey: String;
}
export interface PromotionRecordSchema {
_id: string;
userId: string; // 收益人
objUId?: string; // 目标对象(如果是withdraw则为空)
type: 'invite' | 'shareModel' | 'withdraw';
createTime: Date; // 记录时间
amount: number;
}
export interface ShareChatSchema {
_id: string;
userId: string;
modelId: string;
password: string;
name: string;
tokens: number;
maxContext: number;
lastTime: Date;
}
export interface kbSchema {
_id: string;
userId: string;
updateTime: Date;
avatar: string;
name: string;
tags: string[];
}
export interface informSchema {
_id: string;
userId: string;
time: Date;
type: `${InformTypeEnum}`;
title: string;
content: string;
read: boolean;
}
export interface UserOpenApiKey {
id: string;
apiKey: string;
createTime: Date;
lastUsedTime?: Date;
}
import axios from 'axios';
import { Obj2Query } from '../tools';
export const getClientToken = (googleVerKey: string) => {
if (typeof grecaptcha === 'undefined' || !grecaptcha?.ready) return '';
return new Promise<string>((resolve, reject) => {
grecaptcha.ready(async () => {
try {
const token = await grecaptcha.execute(googleVerKey, {
action: 'submit'
});
resolve(token);
} catch (error) {
reject(error);
}
});
});
};
// service run
export const authGoogleToken = async (data: {
secret: string;
response: string;
remoteip?: string;
}) => {
const res = await axios.post<{
score?: number;
success: boolean;
'error-codes': string[];
}>(`https://www.recaptcha.net/recaptcha/api/siteverify?${Obj2Query(data)}`);
if (res.data.success) {
return Promise.resolve('');
}
return Promise.reject(res?.data?.['error-codes']?.[0] || '非法环境');
};
import { ClaudeEnum, OpenAiChatEnum } from '@/constants/model';
import type { ChatModelType } from '@/constants/model';
import type { ChatItemSimpleType } from '@/types/chat';
import { countOpenAIToken, openAiSliceTextByToken } from './openai';
import { gpt_chatItemTokenSlice } from '@/pages/api/openapi/text/gptMessagesSlice';
export const modelToolMap: Record<
ChatModelType,
{
countTokens: (data: { messages: ChatItemSimpleType[] }) => number;
sliceText: (data: { text: string; length: number }) => string;
tokenSlice: (data: {
messages: ChatItemSimpleType[];
maxToken: number;
}) => ChatItemSimpleType[];
}
> = {
[OpenAiChatEnum.GPT35]: {
countTokens: ({ messages }) => countOpenAIToken({ model: OpenAiChatEnum.GPT35, messages }),
sliceText: (data) => openAiSliceTextByToken({ model: OpenAiChatEnum.GPT35, ...data }),
tokenSlice: (data) => gpt_chatItemTokenSlice({ model: OpenAiChatEnum.GPT35, ...data })
},
[OpenAiChatEnum.GPT4]: {
countTokens: ({ messages }) => countOpenAIToken({ model: OpenAiChatEnum.GPT4, messages }),
sliceText: (data) => openAiSliceTextByToken({ model: OpenAiChatEnum.GPT4, ...data }),
tokenSlice: (data) => gpt_chatItemTokenSlice({ model: OpenAiChatEnum.GPT4, ...data })
},
[OpenAiChatEnum.GPT432k]: {
countTokens: ({ messages }) => countOpenAIToken({ model: OpenAiChatEnum.GPT432k, messages }),
sliceText: (data) => openAiSliceTextByToken({ model: OpenAiChatEnum.GPT432k, ...data }),
tokenSlice: (data) => gpt_chatItemTokenSlice({ model: OpenAiChatEnum.GPT432k, ...data })
},
[ClaudeEnum.Claude]: {
countTokens: ({ messages }) => countOpenAIToken({ model: OpenAiChatEnum.GPT35, messages }),
sliceText: (data) => openAiSliceTextByToken({ model: OpenAiChatEnum.GPT35, ...data }),
tokenSlice: (data) => gpt_chatItemTokenSlice({ model: OpenAiChatEnum.GPT35, ...data })
}
};
import { encoding_for_model, type Tiktoken } from '@dqbd/tiktoken';
import type { ChatItemSimpleType } from '@/types/chat';
import { ChatRoleEnum } from '@/constants/chat';
import { ChatCompletionRequestMessage, ChatCompletionRequestMessageRoleEnum } from 'openai';
import { OpenAiChatEnum } from '@/constants/model';
import Graphemer from 'graphemer';
const textDecoder = new TextDecoder();
const graphemer = new Graphemer();
export const getOpenAiEncMap = () => {
if (typeof window !== 'undefined') {
window.OpenAiEncMap = window.OpenAiEncMap || {
'gpt-3.5-turbo': encoding_for_model('gpt-3.5-turbo', {
'<|im_start|>': 100264,
'<|im_end|>': 100265,
'<|im_sep|>': 100266
}),
'gpt-4': encoding_for_model('gpt-4', {
'<|im_start|>': 100264,
'<|im_end|>': 100265,
'<|im_sep|>': 100266
}),
'gpt-4-32k': encoding_for_model('gpt-4-32k', {
'<|im_start|>': 100264,
'<|im_end|>': 100265,
'<|im_sep|>': 100266
})
};
return window.OpenAiEncMap;
}
if (typeof global !== 'undefined') {
global.OpenAiEncMap = global.OpenAiEncMap || {
'gpt-3.5-turbo': encoding_for_model('gpt-3.5-turbo', {
'<|im_start|>': 100264,
'<|im_end|>': 100265,
'<|im_sep|>': 100266
}),
'gpt-4': encoding_for_model('gpt-4', {
'<|im_start|>': 100264,
'<|im_end|>': 100265,
'<|im_sep|>': 100266
}),
'gpt-4-32k': encoding_for_model('gpt-4-32k', {
'<|im_start|>': 100264,
'<|im_end|>': 100265,
'<|im_sep|>': 100266
})
};
return global.OpenAiEncMap;
}
return {
'gpt-3.5-turbo': encoding_for_model('gpt-3.5-turbo', {
'<|im_start|>': 100264,
'<|im_end|>': 100265,
'<|im_sep|>': 100266
}),
'gpt-4': encoding_for_model('gpt-4', {
'<|im_start|>': 100264,
'<|im_end|>': 100265,
'<|im_sep|>': 100266
}),
'gpt-4-32k': encoding_for_model('gpt-4-32k', {
'<|im_start|>': 100264,
'<|im_end|>': 100265,
'<|im_sep|>': 100266
})
};
};
export const adaptChatItem_openAI = ({
messages
}: {
messages: ChatItemSimpleType[];
}): ChatCompletionRequestMessage[] => {
const map = {
[ChatRoleEnum.AI]: ChatCompletionRequestMessageRoleEnum.Assistant,
[ChatRoleEnum.Human]: ChatCompletionRequestMessageRoleEnum.User,
[ChatRoleEnum.System]: ChatCompletionRequestMessageRoleEnum.System
};
return messages.map((item) => ({
role: map[item.obj] || ChatCompletionRequestMessageRoleEnum.System,
content: item.value || ''
}));
};
export function countOpenAIToken({
messages,
model
}: {
messages: ChatItemSimpleType[];
model: `${OpenAiChatEnum}`;
}) {
function getChatGPTEncodingText(
messages: {
role: 'system' | 'user' | 'assistant';
content: string;
name?: string;
}[],
model: 'gpt-3.5-turbo' | 'gpt-4' | 'gpt-4-32k'
) {
const isGpt3 = model === 'gpt-3.5-turbo';
const msgSep = isGpt3 ? '\n' : '';
const roleSep = isGpt3 ? '\n' : '<|im_sep|>';
return [
messages
.map(({ name = '', role, content }) => {
return `<|im_start|>${name || role}${roleSep}${content}<|im_end|>`;
})
.join(msgSep),
`<|im_start|>assistant${roleSep}`
].join(msgSep);
}
function text2TokensLen(encoder: Tiktoken, inputText: string) {
const encoding = encoder.encode(inputText, 'all');
const segments: { text: string; tokens: { id: number; idx: number }[] }[] = [];
let byteAcc: number[] = [];
let tokenAcc: { id: number; idx: number }[] = [];
let inputGraphemes = graphemer.splitGraphemes(inputText);
for (let idx = 0; idx < encoding.length; idx++) {
const token = encoding[idx]!;
byteAcc.push(...encoder.decode_single_token_bytes(token));
tokenAcc.push({ id: token, idx });
const segmentText = textDecoder.decode(new Uint8Array(byteAcc));
const graphemes = graphemer.splitGraphemes(segmentText);
if (graphemes.every((item, idx) => inputGraphemes[idx] === item)) {
segments.push({ text: segmentText, tokens: tokenAcc });
byteAcc = [];
tokenAcc = [];
inputGraphemes = inputGraphemes.slice(graphemes.length);
}
}
return segments.reduce((memo, i) => memo + i.tokens.length, 0) ?? 0;
}
const adaptMessages = adaptChatItem_openAI({ messages });
return text2TokensLen(getOpenAiEncMap()[model], getChatGPTEncodingText(adaptMessages, model));
}
export const openAiSliceTextByToken = ({
model = 'gpt-3.5-turbo',
text,
length
}: {
model: `${OpenAiChatEnum}`;
text: string;
length: number;
}) => {
const enc = getOpenAiEncMap()[model];
const encodeText = enc.encode(text);
const decoder = new TextDecoder();
return decoder.decode(enc.decode(encodeText.slice(0, length)));
};
import crypto from 'crypto';
import { useToast } from '@/hooks/useToast';
import dayjs from 'dayjs';
/**
* copy text data
*/
export const useCopyData = () => {
const { toast } = useToast();
return {
copyData: async (data: string, title: string = '复制成功') => {
try {
if (navigator.clipboard) {
await navigator.clipboard.writeText(data);
} else {
throw new Error('');
}
} catch (error) {
const textarea = document.createElement('textarea');
textarea.value = data;
document.body.appendChild(textarea);
textarea.select();
document.execCommand('copy');
document.body.removeChild(textarea);
}
toast({
title,
status: 'success',
duration: 1000
});
}
};
};
/**
* 密码加密
*/
export const createHashPassword = (text: string) => {
const hash = crypto.createHash('sha256').update(text).digest('hex');
return hash;
};
/**
* 对象转成 query 字符串
*/
export const Obj2Query = (obj: Record<string, string | number>) => {
const queryParams = new URLSearchParams();
for (const key in obj) {
queryParams.append(key, `${obj[key]}`);
}
return queryParams.toString();
};
/**
* 格式化时间成聊天格式
*/
export const formatTimeToChatTime = (time: Date) => {
const now = dayjs();
const target = dayjs(time);
// 如果传入时间小于60秒,返回刚刚
if (now.diff(target, 'second') < 60) {
return '刚刚';
}
// 如果时间是今天,展示几时:几秒
if (now.isSame(target, 'day')) {
return target.format('HH:mm');
}
// 如果是昨天,展示昨天
if (now.subtract(1, 'day').isSame(target, 'day')) {
return '昨天';
}
// 如果是前天,展示前天
if (now.subtract(2, 'day').isSame(target, 'day')) {
return '前天';
}
// 如果是今年,展示某月某日
if (now.isSame(target, 'year')) {
return target.format('M月D日');
}
// 如果是更久之前,展示某年某月某日
return target.format('YYYY/M/D');
};
export const hasVoiceApi = typeof window !== 'undefined' && 'speechSynthesis' in window;
/**
* voice broadcast
*/
export const voiceBroadcast = ({ text }: { text: string }) => {
window.speechSynthesis?.cancel();
const msg = new SpeechSynthesisUtterance(text);
const voices = window.speechSynthesis?.getVoices?.(); // 获取语言包
const voice = voices.find((item) => {
return item.name === 'Microsoft Yaoyao - Chinese (Simplified, PRC)';
});
if (voice) {
msg.voice = voice;
}
window.speechSynthesis?.speak(msg);
msg.onerror = (e) => {
console.log(e);
};
return {
cancel: () => window.speechSynthesis?.cancel()
};
};
export const formatLinkText = (text: string) => {
const httpReg =
/(http|https|ftp):\/\/[\w\-_]+(\.[\w\-_]+)+([\w\-\.,@?^=%&amp;:/~\+#]*[\w\-\@?^=%&amp;/~\+#])?/gi;
return text.replace(httpReg, ` $& `);
};
export const getErrText = (err: any, def = '') => {
const msg = typeof err === 'string' ? err : err?.message || def || '';
msg && console.log('error =>', msg);
return msg;
};
export const delay = (ms: number) =>
new Promise((resolve) => {
setTimeout(() => {
resolve('');
}, ms);
});
{
"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": ".",
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "**/*.d.ts"],
"exclude": ["node_modules"]
}
QA_MAX_PROCESS=30
VECTOR_MAX_PROCESS=30
# 运行端口,如果不是 3000 口运行,需要改成其他的。注意:不是改了这个变量就会变成其他端口,而是因为改成其他端口,才用这个变量。
PORT=3000
# 代理
# AXIOS_PROXY_HOST=127.0.0.1
# AXIOS_PROXY_PORT=7890
# email
MY_MAIL=xxx@qq.com
MAILE_CODE=xxx
MY_MAIL=xxxx@qq.com
MAILE_CODE=xxxx
# ali ems
aliAccessKeyId=xxx
aliAccessKeySecret=xxx
aliSignName=xxx
aliTemplateCode=SMS_xxx
aliAccessKeyId=xxxx
aliAccessKeySecret=xxxx
aliSignName=xxxx
aliTemplateCode=xxxx
# token
TOKEN_KEY=xxx
TOKEN_KEY=dfdasfdas
# root key, 最高权限
ROOT_KEY=xxx
ROOT_KEY=fdafasd
# 是否进行安全校验(1: 开启,0: 关闭)
SENSITIVE_CHECK=1
SENSITIVE_CHECK=0
# openai
# OPENAI_BASE_URL=https://api.openai.com/v1
# OPENAI_BASE_URL_AUTH=可选的安全凭证(不需要的时候,记得去掉)
OPENAIKEY=sk-xxx # 对话用的key
OPENAI_TRAINING_KEY=sk-xxx # 训练用的key
# OPENAI_BASE_URL=http://ai.openai.com/v1
# OPENAI_BASE_URL_AUTH=可选安全凭证,会放到 header.auth 里
OPENAIKEY=sk-xxx
OPENAI_TRAINING_KEY=sk-xxx
GPT4KEY=sk-xxx
# db
MONGODB_URI=mongodb://username:password@0.0.0.0:27017/test?authSource=admin
MONGODB_URI=mongodb://username:password@0.0.0.0:27017/?authSource=admin
MONGODB_NAME=fastgpt
PG_HOST=0.0.0.0
PG_PORT=8100
PG_USER=xxx
PG_PASSWORD=xxx
PG_DB_NAME=xxx
\ No newline at end of file
PG_USER=root
PG_PASSWORD=psw
PG_DB_NAME=dbname
\ No newline at end of file
......@@ -11,3 +11,10 @@
```
pnpm dev
```
## 镜像打包
```bash
# 代理可选,不需要的去掉
docker build -t registry.cn-hangzhou.aliyuncs.com/fastgpt/fastgpt:latest . --network host --build-arg HTTP_PROXY=http://127.0.0.1:7890 --build-arg HTTPS_PROXY=http://127.0.0.1:7890
```
......@@ -3,86 +3,17 @@
"version": "3.7",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint",
"prepare": "husky install",
"format": "prettier --config \"./.prettierrc.js\" --write \"./src/**/*.{ts,tsx,scss}\""
},
"dependencies": {
"@alicloud/dysmsapi20170525": "^2.0.23",
"@alicloud/openapi-client": "^0.4.5",
"@alicloud/tea-util": "^1.4.5",
"@chakra-ui/icons": "^2.0.17",
"@chakra-ui/react": "^2.5.1",
"@chakra-ui/system": "^2.5.5",
"@dqbd/tiktoken": "^1.0.6",
"@emotion/react": "^11.10.6",
"@emotion/styled": "^11.10.6",
"@next/font": "13.1.6",
"@tanstack/react-query": "^4.24.10",
"@types/nprogress": "^0.2.0",
"axios": "^1.3.3",
"cookie": "^0.5.0",
"crypto": "^1.0.1",
"dayjs": "^1.11.7",
"eventsource-parser": "^0.1.0",
"formidable": "^2.1.1",
"framer-motion": "^9.0.6",
"graphemer": "^1.4.0",
"hyperdown": "^2.4.29",
"immer": "^9.0.19",
"jsonwebtoken": "^9.0.0",
"lodash": "^4.17.21",
"mammoth": "^1.5.1",
"mongoose": "^6.10.0",
"nanoid": "^4.0.1",
"next": "13.1.6",
"nextjs-cors": "^2.1.2",
"nodemailer": "^6.9.1",
"nprogress": "^0.2.0",
"openai": "^3.2.1",
"papaparse": "^5.4.1",
"pg": "^8.10.0",
"react": "18.2.0",
"react-dom": "18.2.0",
"react-hook-form": "^7.43.1",
"react-markdown": "^8.0.5",
"react-syntax-highlighter": "^15.5.0",
"rehype-katex": "^6.0.2",
"remark-gfm": "^3.0.1",
"remark-math": "^5.1.1",
"request-ip": "^3.3.0",
"sass": "^1.58.3",
"tunnel": "^0.0.6",
"wxpay-v3": "^3.0.2",
"zustand": "^4.3.5"
"format": "prettier --config \"./.prettierrc.js\" --write \"./**/src/**/*.{ts,tsx,scss}\""
},
"dependencies": {},
"devDependencies": {
"@svgr/webpack": "^6.5.1",
"@types/cookie": "^0.5.1",
"@types/formidable": "^2.0.5",
"@types/jsonwebtoken": "^9.0.1",
"@types/lodash": "^4.14.191",
"@types/node": "18.14.0",
"@types/nodemailer": "^6.4.7",
"@types/papaparse": "^5.3.7",
"@types/pg": "^8.6.6",
"@types/react": "18.0.28",
"@types/react-dom": "18.0.11",
"@types/react-syntax-highlighter": "^15.5.6",
"@types/request-ip": "^0.0.37",
"@types/tunnel": "^0.0.3",
"eslint": "8.34.0",
"eslint-config-next": "13.1.6",
"husky": "^8.0.3",
"lint-staged": "^13.1.2",
"prettier": "^2.8.4",
"typescript": "4.9.5"
"lint-staged": "^13.2.1",
"prettier": "^2.8.7"
},
"lint-staged": {
"./src/**/*.{ts,tsx,scss}": "npm run format"
"./**/src/**/*.{ts,tsx,scss}": "npm run format"
},
"engines": {
"node": ">=18.0.0"
......
This source diff could not be displayed because it is too large. You can view the blob instead.
import { Schema, model, models, Model } from 'mongoose';
import { hashPassword } from '@/service/utils/tools';
import { PRICE_SCALE } from '@/constants/common';
import { UserModelSchema } from '@/types/mongoSchema';
const UserSchema = new Schema({
username: {
// 可以是手机/邮箱,新的验证都只用手机
type: String,
required: true,
unique: true // 唯一
},
password: {
type: String,
required: true,
set: (val: string) => hashPassword(val),
get: (val: string) => hashPassword(val),
select: false
},
createTime: {
type: Date,
default: () => new Date()
},
avatar: {
type: String,
default: '/icon/human.png'
},
balance: {
// 平台余额,不可提现
type: Number,
default: 2 * PRICE_SCALE
},
inviterId: {
// 谁邀请注册的
type: Schema.Types.ObjectId,
ref: 'user'
},
promotion: {
rate: {
// 返现比例
type: Number,
default: 15
}
},
openaiKey: {
type: String,
default: ''
},
limit: {
exportKbTime: {
// Every half hour
type: Date
}
}
});
export const User: Model<UserModelSchema> = models['user'] || model('user', UserSchema);
import mongoose from 'mongoose';
import tunnel from 'tunnel';
import { TrainingData } from './mongo';
import { startQueue } from './utils/tools';
/**
* 连接 MongoDB 数据库
*/
export async function connectToDatabase(): Promise<void> {
if (global.mongodb) {
return;
}
global.mongodb = 'connecting';
try {
mongoose.set('strictQuery', true);
global.mongodb = await mongoose.connect(process.env.MONGODB_URI as string, {
bufferCommands: true,
dbName: process.env.MONGODB_NAME,
maxPoolSize: 5,
minPoolSize: 1,
maxConnecting: 5
});
console.log('mongo connected');
} catch (error) {
console.log('error->', 'mongo connect error');
global.mongodb = null;
}
// 创建代理对象
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
}
});
}
// 初始化队列
global.qaQueueLen = 0;
global.vectorQueueLen = 0;
startQueue();
}
export * from './models/authCode';
export * from './models/chat';
export * from './models/model';
export * from './models/user';
export * from './models/bill';
export * from './models/pay';
export * from './models/trainingData';
export * from './models/openapi';
export * from './models/promotionRecord';
export * from './models/collection';
export * from './models/shareChat';
export * from './models/kb';
export * from './models/inform';
import { NextApiResponse } from 'next';
import {
openaiError,
openaiAccountError,
proxyError,
ERROR_RESPONSE,
ERROR_ENUM
} from './errorCode';
import { clearCookie } from './utils/tools';
export interface ResponseType<T = any> {
code: number;
message: string;
data: T;
}
export const jsonRes = <T = any>(
res: NextApiResponse,
props?: {
code?: number;
message?: string;
data?: T;
error?: any;
}
) => {
const { code = 200, message = '', data = null, error } = props || {};
const errResponseKey = typeof error === 'string' ? error : error?.message;
// Specified error
if (ERROR_RESPONSE[errResponseKey]) {
// login is expired
if (errResponseKey === ERROR_ENUM.unAuthorization) {
clearCookie(res);
}
return res.json(ERROR_RESPONSE[errResponseKey]);
}
// another error
let msg = message || error?.message;
if ((code < 200 || code >= 400) && !message) {
msg = error?.message || '请求错误';
if (typeof error === 'string') {
msg = error;
} else if (proxyError[error?.code]) {
msg = '接口连接异常';
} else if (error?.response?.data?.error?.message) {
msg = error?.response?.data?.error?.message;
} else if (openaiAccountError[error?.response?.data?.error?.code]) {
msg = openaiAccountError[error?.response?.data?.error?.code];
} else if (openaiError[error?.response?.statusText]) {
msg = openaiError[error.response.statusText];
}
console.log(error);
}
res.json({
code,
statusText: '',
message: msg,
data: data !== undefined ? data : null
});
};
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);
}
};
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);
}
};
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 || '发送短信失败');
}
};
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();
}
};
// @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));
import { create } from 'zustand';
import { devtools } from 'zustand/middleware';
import { immer } from 'zustand/middleware/immer';
import type { InitDateResponse } from '@/pages/api/system/getInitData';
import { getInitData } from '@/api/system';
type State = {
initData: InitDateResponse;
loadInitData: () => Promise<void>;
loading: boolean;
setLoading: (val: boolean) => null;
screenWidth: number;
setScreenWidth: (val: number) => void;
isPc: boolean;
};
export const useGlobalStore = create<State>()(
devtools(
immer((set, get) => ({
initData: {
beianText: '',
googleVerKey: ''
},
async loadInitData() {
try {
const res = await getInitData();
set((state) => {
state.initData = res;
});
} catch (error) {}
},
loading: false,
setLoading: (val: boolean) => {
set((state) => {
state.loading = val;
});
return null;
},
screenWidth: 600,
setScreenWidth(val: number) {
set((state) => {
state.screenWidth = val;
state.isPc = val < 900 ? false : true;
});
},
isPc: false
}))
)
);
body,
h1,
h2,
h3,
h4,
hr,
p,
blockquote,
dl,
dt,
dd,
ul,
ol,
li,
pre,
form,
fieldset,
legend,
button,
input,
textarea,
th,
td,
svg {
margin: 0;
}
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: transparent;
border-radius: 2px;
}
::-webkit-scrollbar-thumb {
background: rgba(189, 193, 197, 0.7);
border-radius: 2px;
}
::-webkit-scrollbar-thumb:hover {
background: rgba(189, 193, 197, 1);
}
div {
&::-webkit-scrollbar-thumb {
background: transparent !important;
transition: 1s;
}
&:hover {
&::-webkit-scrollbar-thumb {
background: rgba(189, 193, 197, 0.7) !important;
}
&::-webkit-scrollbar-thumb:hover {
background: rgba(189, 193, 197, 1) !important;
}
}
}
input::placeholder,
textarea::placeholder {
font-size: 0.85em;
}
* {
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
-webkit-focus-ring-color: rgba(0, 0, 0, 0);
outline: none;
}
#__next {
height: 100%;
}
#nprogress .bar {
background: '#85b1ff' !important; //自定义颜色
}
.textEllipsis {
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
}
.grecaptcha-badge {
display: none !important;
}
@media (max-width: 900px) {
html {
font-size: 14px;
}
::-webkit-scrollbar {
width: 2px;
height: 2px;
}
}
@supports (bottom: constant(safe-area-inset-bottom)) or (bottom: env(safe-area-inset-bottom)) {
body {
padding-bottom: constant(safe-area-inset-bottom);
padding-bottom: env(safe-area-inset-bottom);
}
}
import { ChatRoleEnum } from '@/constants/chat';
import type { InitChatResponse, InitShareChatResponse } from '@/api/response/chat';
import { QuoteItemType } from '@/pages/api/openapi/kb/appKbSearch';
export type ExportChatType = 'md' | 'pdf' | 'html';
export type ChatItemSimpleType = {
obj: `${ChatRoleEnum}`;
value: string;
quoteLen?: number;
quote?: QuoteItemType[];
systemPrompt?: string;
};
export type ChatItemType = {
_id: string;
} & ChatItemSimpleType;
export type ChatSiteItemType = {
status: 'loading' | 'finish';
} & ChatItemType;
export interface ChatType extends InitChatResponse {
history: ChatSiteItemType[];
}
export interface ShareChatType extends InitShareChatResponse {
history: ChatSiteItemType[];
}
export type HistoryItemType = {
_id: string;
updateTime: Date;
modelId: string;
title: string;
latestChat: string;
top: boolean;
};
export type ShareChatHistoryItemType = {
_id: string;
shareId: string;
updateTime: Date;
title: string;
latestChat: string;
chats: ChatSiteItemType[];
};
import type { Mongoose } from 'mongoose';
import type { Agent } from 'http';
import type { Pool } from 'pg';
import type { Tiktoken } from '@dqbd/tiktoken';
declare global {
var mongodb: Mongoose | string | null;
var pgClient: Pool | null;
var httpsAgent: Agent;
var particlesJS: any;
var grecaptcha: any;
var QRCode: any;
var qaQueueLen: number;
var vectorQueueLen: number;
var OpenAiEncMap: Record<string, Tiktoken>;
interface Window {
['pdfjs-dist/build/pdf']: any;
}
}
export type PagingData<T> = {
pageNum: number;
pageSize: number;
data: T[];
total?: number;
};
export type RequestPaging = { pageNum: number; pageSize: number; [key]: any };
import { ModelStatusEnum } from '@/constants/model';
import type { ModelSchema, kbSchema } from './mongoSchema';
import { ChatModelType, appVectorSearchModeEnum } from '@/constants/model';
export type ModelListItemType = {
_id: string;
name: string;
avatar: string;
systemPrompt: string;
};
export interface ModelUpdateParams {
name: string;
avatar: string;
chat: ModelSchema['chat'];
share: ModelSchema['share'];
}
export interface ShareModelItem {
_id: string;
avatar: string;
name: string;
userId: string;
share: ModelSchema['share'];
isCollection: boolean;
}
export type ShareChatEditType = {
name: string;
password: string;
maxContext: number;
};
import type { ChatItemType } from './chat';
import {
ModelStatusEnum,
ModelNameEnum,
appVectorSearchModeEnum,
ChatModelType,
EmbeddingModelType
} from '@/constants/model';
import type { DataType } from './data';
import { BillTypeEnum, InformTypeEnum } from '@/constants/user';
import { TrainingModeEnum } from '@/constants/plugin';
export interface UserModelSchema {
_id: string;
username: string;
password: string;
avatar: string;
balance: number;
inviterId?: string;
promotionAmount: number;
openaiKey: string;
createTime: number;
promotion: {
rate: number;
};
limit: {
exportKbTime?: Date;
};
}
export interface AuthCodeSchema {
_id: string;
username: string;
code: string;
type: 'register' | 'findPassword';
expiredTime: number;
}
export interface ModelSchema {
_id: string;
userId: string;
name: string;
avatar: string;
status: `${ModelStatusEnum}`;
updateTime: number;
chat: {
relatedKbs: string[];
searchMode: `${appVectorSearchModeEnum}`;
systemPrompt: string;
temperature: number;
chatModel: ChatModelType; // 聊天时用的模型,训练后就是训练的模型
};
share: {
isShare: boolean;
isShareDetail: boolean;
intro: string;
collection: number;
};
}
export interface ModelPopulate extends ModelSchema {
userId: UserModelSchema;
}
export interface CollectionSchema {
modelId: string;
userId: string;
}
export type ModelDataType = 0 | 1;
export interface TrainingDataSchema {
_id: string;
userId: string;
kbId: string;
lockTime: Date;
mode: `${TrainingModeEnum}`;
prompt: string;
q: string;
a: string;
source: string;
}
export interface ChatSchema {
_id: string;
userId: string;
modelId: string;
expiredTime: number;
updateTime: Date;
title: string;
customTitle: string;
latestChat: string;
top: boolean;
content: ChatItemType[];
}
export interface ChatPopulate extends ChatSchema {
userId: UserModelSchema;
modelId: ModelSchema;
}
export interface BillSchema {
_id: string;
userId: string;
type: `${BillTypeEnum}`;
modelName: ChatModelType | EmbeddingModelType;
chatId: string;
time: Date;
textLen: number;
tokenLen: number;
price: number;
}
export interface PaySchema {
_id: string;
userId: string;
createTime: Date;
price: number;
orderId: string;
status: 'SUCCESS' | 'REFUND' | 'NOTPAY' | 'CLOSED';
}
export interface OpenApiSchema {
_id: string;
userId: string;
createTime: Date;
lastUsedTime?: Date;
apiKey: String;
}
export interface PromotionRecordSchema {
_id: string;
userId: string; // 收益人
objUId?: string; // 目标对象(如果是withdraw则为空)
type: 'invite' | 'shareModel' | 'withdraw';
createTime: Date; // 记录时间
amount: number;
}
export interface ShareChatSchema {
_id: string;
userId: string;
modelId: string;
password: string;
name: string;
tokens: number;
maxContext: number;
lastTime: Date;
}
export interface kbSchema {
_id: string;
userId: string;
updateTime: Date;
avatar: string;
name: string;
tags: string[];
}
export interface informSchema {
_id: string;
userId: string;
time: Date;
type: `${InformTypeEnum}`;
title: string;
content: string;
read: boolean;
}
export interface UserOpenApiKey {
id: string;
apiKey: string;
createTime: Date;
lastUsedTime?: Date;
}
import axios from 'axios';
import { Obj2Query } from '../tools';
export const getClientToken = (googleVerKey: string) => {
if (typeof grecaptcha === 'undefined' || !grecaptcha?.ready) return '';
return new Promise<string>((resolve, reject) => {
grecaptcha.ready(async () => {
try {
const token = await grecaptcha.execute(googleVerKey, {
action: 'submit'
});
resolve(token);
} catch (error) {
reject(error);
}
});
});
};
// service run
export const authGoogleToken = async (data: {
secret: string;
response: string;
remoteip?: string;
}) => {
const res = await axios.post<{ score?: number; success: boolean; 'error-codes': string[] }>(
`https://www.recaptcha.net/recaptcha/api/siteverify?${Obj2Query(data)}`
);
if (res.data.success) {
return Promise.resolve('');
}
return Promise.reject(res?.data?.['error-codes']?.[0] || '非法环境');
};
import { ClaudeEnum, OpenAiChatEnum } from '@/constants/model';
import type { ChatModelType } from '@/constants/model';
import type { ChatItemSimpleType } from '@/types/chat';
import { countOpenAIToken, openAiSliceTextByToken } from './openai';
import { gpt_chatItemTokenSlice } from '@/pages/api/openapi/text/gptMessagesSlice';
export const modelToolMap: Record<
ChatModelType,
{
countTokens: (data: { messages: ChatItemSimpleType[] }) => number;
sliceText: (data: { text: string; length: number }) => string;
tokenSlice: (data: {
messages: ChatItemSimpleType[];
maxToken: number;
}) => ChatItemSimpleType[];
}
> = {
[OpenAiChatEnum.GPT35]: {
countTokens: ({ messages }) => countOpenAIToken({ model: OpenAiChatEnum.GPT35, messages }),
sliceText: (data) => openAiSliceTextByToken({ model: OpenAiChatEnum.GPT35, ...data }),
tokenSlice: (data) => gpt_chatItemTokenSlice({ model: OpenAiChatEnum.GPT35, ...data })
},
[OpenAiChatEnum.GPT4]: {
countTokens: ({ messages }) => countOpenAIToken({ model: OpenAiChatEnum.GPT4, messages }),
sliceText: (data) => openAiSliceTextByToken({ model: OpenAiChatEnum.GPT4, ...data }),
tokenSlice: (data) => gpt_chatItemTokenSlice({ model: OpenAiChatEnum.GPT4, ...data })
},
[OpenAiChatEnum.GPT432k]: {
countTokens: ({ messages }) => countOpenAIToken({ model: OpenAiChatEnum.GPT432k, messages }),
sliceText: (data) => openAiSliceTextByToken({ model: OpenAiChatEnum.GPT432k, ...data }),
tokenSlice: (data) => gpt_chatItemTokenSlice({ model: OpenAiChatEnum.GPT432k, ...data })
},
[ClaudeEnum.Claude]: {
countTokens: ({ messages }) => countOpenAIToken({ model: OpenAiChatEnum.GPT35, messages }),
sliceText: (data) => openAiSliceTextByToken({ model: OpenAiChatEnum.GPT35, ...data }),
tokenSlice: (data) => gpt_chatItemTokenSlice({ model: OpenAiChatEnum.GPT35, ...data })
}
};
import { encoding_for_model, type Tiktoken } from '@dqbd/tiktoken';
import type { ChatItemSimpleType } from '@/types/chat';
import { ChatRoleEnum } from '@/constants/chat';
import { ChatCompletionRequestMessage, ChatCompletionRequestMessageRoleEnum } from 'openai';
import { OpenAiChatEnum } from '@/constants/model';
import Graphemer from 'graphemer';
const textDecoder = new TextDecoder();
const graphemer = new Graphemer();
export const getOpenAiEncMap = () => {
if (typeof window !== 'undefined') {
window.OpenAiEncMap = window.OpenAiEncMap || {
'gpt-3.5-turbo': encoding_for_model('gpt-3.5-turbo', {
'<|im_start|>': 100264,
'<|im_end|>': 100265,
'<|im_sep|>': 100266
}),
'gpt-4': encoding_for_model('gpt-4', {
'<|im_start|>': 100264,
'<|im_end|>': 100265,
'<|im_sep|>': 100266
}),
'gpt-4-32k': encoding_for_model('gpt-4-32k', {
'<|im_start|>': 100264,
'<|im_end|>': 100265,
'<|im_sep|>': 100266
})
};
return window.OpenAiEncMap;
}
if (typeof global !== 'undefined') {
global.OpenAiEncMap = global.OpenAiEncMap || {
'gpt-3.5-turbo': encoding_for_model('gpt-3.5-turbo', {
'<|im_start|>': 100264,
'<|im_end|>': 100265,
'<|im_sep|>': 100266
}),
'gpt-4': encoding_for_model('gpt-4', {
'<|im_start|>': 100264,
'<|im_end|>': 100265,
'<|im_sep|>': 100266
}),
'gpt-4-32k': encoding_for_model('gpt-4-32k', {
'<|im_start|>': 100264,
'<|im_end|>': 100265,
'<|im_sep|>': 100266
})
};
return global.OpenAiEncMap;
}
return {
'gpt-3.5-turbo': encoding_for_model('gpt-3.5-turbo', {
'<|im_start|>': 100264,
'<|im_end|>': 100265,
'<|im_sep|>': 100266
}),
'gpt-4': encoding_for_model('gpt-4', {
'<|im_start|>': 100264,
'<|im_end|>': 100265,
'<|im_sep|>': 100266
}),
'gpt-4-32k': encoding_for_model('gpt-4-32k', {
'<|im_start|>': 100264,
'<|im_end|>': 100265,
'<|im_sep|>': 100266
})
};
};
export const adaptChatItem_openAI = ({
messages
}: {
messages: ChatItemSimpleType[];
}): ChatCompletionRequestMessage[] => {
const map = {
[ChatRoleEnum.AI]: ChatCompletionRequestMessageRoleEnum.Assistant,
[ChatRoleEnum.Human]: ChatCompletionRequestMessageRoleEnum.User,
[ChatRoleEnum.System]: ChatCompletionRequestMessageRoleEnum.System
};
return messages.map((item) => ({
role: map[item.obj] || ChatCompletionRequestMessageRoleEnum.System,
content: item.value || ''
}));
};
export function countOpenAIToken({
messages,
model
}: {
messages: ChatItemSimpleType[];
model: `${OpenAiChatEnum}`;
}) {
function getChatGPTEncodingText(
messages: { role: 'system' | 'user' | 'assistant'; content: string; name?: string }[],
model: 'gpt-3.5-turbo' | 'gpt-4' | 'gpt-4-32k'
) {
const isGpt3 = model === 'gpt-3.5-turbo';
const msgSep = isGpt3 ? '\n' : '';
const roleSep = isGpt3 ? '\n' : '<|im_sep|>';
return [
messages
.map(({ name = '', role, content }) => {
return `<|im_start|>${name || role}${roleSep}${content}<|im_end|>`;
})
.join(msgSep),
`<|im_start|>assistant${roleSep}`
].join(msgSep);
}
function text2TokensLen(encoder: Tiktoken, inputText: string) {
const encoding = encoder.encode(inputText, 'all');
const segments: { text: string; tokens: { id: number; idx: number }[] }[] = [];
let byteAcc: number[] = [];
let tokenAcc: { id: number; idx: number }[] = [];
let inputGraphemes = graphemer.splitGraphemes(inputText);
for (let idx = 0; idx < encoding.length; idx++) {
const token = encoding[idx]!;
byteAcc.push(...encoder.decode_single_token_bytes(token));
tokenAcc.push({ id: token, idx });
const segmentText = textDecoder.decode(new Uint8Array(byteAcc));
const graphemes = graphemer.splitGraphemes(segmentText);
if (graphemes.every((item, idx) => inputGraphemes[idx] === item)) {
segments.push({ text: segmentText, tokens: tokenAcc });
byteAcc = [];
tokenAcc = [];
inputGraphemes = inputGraphemes.slice(graphemes.length);
}
}
return segments.reduce((memo, i) => memo + i.tokens.length, 0) ?? 0;
}
const adaptMessages = adaptChatItem_openAI({ messages });
return text2TokensLen(getOpenAiEncMap()[model], getChatGPTEncodingText(adaptMessages, model));
}
export const openAiSliceTextByToken = ({
model = 'gpt-3.5-turbo',
text,
length
}: {
model: `${OpenAiChatEnum}`;
text: string;
length: number;
}) => {
const enc = getOpenAiEncMap()[model];
const encodeText = enc.encode(text);
const decoder = new TextDecoder();
return decoder.decode(enc.decode(encodeText.slice(0, length)));
};
import crypto from 'crypto';
import { useToast } from '@/hooks/useToast';
import dayjs from 'dayjs';
/**
* copy text data
*/
export const useCopyData = () => {
const { toast } = useToast();
return {
copyData: async (data: string, title: string = '复制成功') => {
try {
if (navigator.clipboard) {
await navigator.clipboard.writeText(data);
} else {
throw new Error('');
}
} catch (error) {
const textarea = document.createElement('textarea');
textarea.value = data;
document.body.appendChild(textarea);
textarea.select();
document.execCommand('copy');
document.body.removeChild(textarea);
}
toast({
title,
status: 'success',
duration: 1000
});
}
};
};
/**
* 密码加密
*/
export const createHashPassword = (text: string) => {
const hash = crypto.createHash('sha256').update(text).digest('hex');
return hash;
};
/**
* 对象转成 query 字符串
*/
export const Obj2Query = (obj: Record<string, string | number>) => {
const queryParams = new URLSearchParams();
for (const key in obj) {
queryParams.append(key, `${obj[key]}`);
}
return queryParams.toString();
};
/**
* 格式化时间成聊天格式
*/
export const formatTimeToChatTime = (time: Date) => {
const now = dayjs();
const target = dayjs(time);
// 如果传入时间小于60秒,返回刚刚
if (now.diff(target, 'second') < 60) {
return '刚刚';
}
// 如果时间是今天,展示几时:几秒
if (now.isSame(target, 'day')) {
return target.format('HH:mm');
}
// 如果是昨天,展示昨天
if (now.subtract(1, 'day').isSame(target, 'day')) {
return '昨天';
}
// 如果是前天,展示前天
if (now.subtract(2, 'day').isSame(target, 'day')) {
return '前天';
}
// 如果是今年,展示某月某日
if (now.isSame(target, 'year')) {
return target.format('M月D日');
}
// 如果是更久之前,展示某年某月某日
return target.format('YYYY/M/D');
};
export const hasVoiceApi = typeof window !== 'undefined' && 'speechSynthesis' in window;
/**
* voice broadcast
*/
export const voiceBroadcast = ({ text }: { text: string }) => {
window.speechSynthesis?.cancel();
const msg = new SpeechSynthesisUtterance(text);
const voices = window.speechSynthesis?.getVoices?.(); // 获取语言包
const voice = voices.find((item) => {
return item.name === 'Microsoft Yaoyao - Chinese (Simplified, PRC)';
});
if (voice) {
msg.voice = voice;
}
window.speechSynthesis?.speak(msg);
msg.onerror = (e) => {
console.log(e);
};
return {
cancel: () => window.speechSynthesis?.cancel()
};
};
export const formatLinkText = (text: string) => {
const httpReg =
/(http|https|ftp):\/\/[\w\-_]+(\.[\w\-_]+)+([\w\-\.,@?^=%&amp;:/~\+#]*[\w\-\@?^=%&amp;/~\+#])?/gi;
return text.replace(httpReg, ` $& `);
};
export const getErrText = (err: any, def = '') => {
const msg = typeof err === 'string' ? err : err?.message || def || '';
msg && console.log('error =>', msg);
return msg;
};
export const delay = (ms: number) =>
new Promise((resolve) => {
setTimeout(() => {
resolve('');
}, ms);
});
{
"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": ".",
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "**/*.d.ts"],
"exclude": ["node_modules"]
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment