chatbox ui

This commit is contained in:
archer
2023-07-11 23:22:01 +08:00
parent eb768d9c04
commit b2e2f60e0d
46 changed files with 1123 additions and 2817 deletions

View File

@@ -7,9 +7,9 @@ import { authApp } from '@/service/utils/auth';
/* 获取我的模型 */
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
try {
const { modelId } = req.query as { modelId: string };
const { appId } = req.query as { appId: string };
if (!modelId) {
if (!appId) {
throw new Error('参数错误');
}
@@ -20,28 +20,28 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
// 验证是否是该用户的 model
await authApp({
appId: modelId,
appId,
userId
});
// 删除对应的聊天
await Chat.deleteMany({
modelId
appId
});
// 删除收藏列表
await Collection.deleteMany({
modelId
modelId: appId
});
// 删除分享链接
await ShareChat.deleteMany({
modelId
appId
});
// 删除模型
await App.deleteOne({
_id: modelId,
_id: appId,
userId
});

View File

@@ -7,9 +7,9 @@ import { authApp } from '@/service/utils/auth';
/* 获取我的模型 */
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
try {
const { modelId } = req.query as { modelId: string };
const { appId } = req.query as { appId: string };
if (!modelId) {
if (!appId) {
throw new Error('参数错误');
}
@@ -19,7 +19,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
await connectToDatabase();
const { app } = await authApp({
appId: modelId,
appId,
userId,
authOwner: false
});

View File

@@ -1,58 +0,0 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { connectToDatabase, Collection, App } from '@/service/mongo';
import { authUser } from '@/service/utils/auth';
import type { AppListResponse } from '@/api/response/app';
/* 获取模型列表 */
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
try {
// 凭证校验
const { userId } = await authUser({ req, authToken: true });
await connectToDatabase();
// 根据 userId 获取模型信息
const [myApps, myCollections] = await Promise.all([
App.find(
{
userId
},
'_id avatar name intro'
).sort({
updateTime: -1
}),
Collection.find({ userId })
.populate({
path: 'modelId',
select: '_id avatar name intro',
match: { 'share.isShare': true }
})
.then((res) => res.filter((item) => item.modelId))
]);
jsonRes<AppListResponse>(res, {
data: {
myApps: myApps.map((item) => ({
_id: item._id,
name: item.name,
avatar: item.avatar,
intro: item.intro
})),
myCollectionApps: myCollections
.map((item: any) => ({
_id: item.modelId?._id,
name: item.modelId?.name,
avatar: item.modelId?.avatar,
intro: item.modelId?.intro
}))
.filter((item) => !myApps.find((model) => String(model._id) === String(item._id))) // 去重
}
});
} catch (err) {
jsonRes(res, {
code: 500,
error: err
});
}
}

View File

@@ -0,0 +1,33 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { connectToDatabase, App } from '@/service/mongo';
import { authUser } from '@/service/utils/auth';
import { AppListItemType } from '@/types/app';
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
try {
// 凭证校验
const { userId } = await authUser({ req, authToken: true });
await connectToDatabase();
// 根据 userId 获取模型信息
const myApps = await App.find(
{
userId
},
'_id avatar name intro'
).sort({
updateTime: -1
});
jsonRes<AppListItemType[]>(res, {
data: myApps
});
} catch (err) {
jsonRes(res, {
code: 500,
error: err
});
}
}

View File

@@ -6,9 +6,9 @@ import { authUser } from '@/service/utils/auth';
/* 模型收藏切换 */
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
try {
const { modelId } = req.query as { modelId: string };
const { appId } = req.query as { appId: string };
if (!modelId) {
if (!appId) {
throw new Error('缺少参数');
}
// 凭证校验
@@ -18,7 +18,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
const collectionRecord = await Collection.findOne({
userId,
modelId
modelId: appId
});
if (collectionRecord) {
@@ -26,12 +26,12 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
} else {
await Collection.create({
userId,
modelId
modelId: appId
});
}
await App.findByIdAndUpdate(modelId, {
'share.collection': await Collection.countDocuments({ modelId })
await App.findByIdAndUpdate(appId, {
'share.collection': await Collection.countDocuments({ modelId: appId })
});
jsonRes(res);

View File

@@ -6,14 +6,15 @@ import { sseResponseEventEnum } from '@/constants/chat';
import { sseResponse } from '@/service/utils/tools';
import { type ChatCompletionRequestMessage } from 'openai';
import { AppModuleItemType } from '@/types/app';
import { dispatchModules } from '../openapi/v1/chat/completions2';
import { dispatchModules } from '../openapi/v1/chat/completions';
import { gptMessage2ChatType } from '@/utils/adapt';
export type MessageItemType = ChatCompletionRequestMessage & { _id?: string };
export type Props = {
history: MessageItemType[];
prompt: string;
modules: AppModuleItemType[];
variable: Record<string, any>;
variables: Record<string, any>;
};
export type ChatResponseType = {
newChatId: string;
@@ -29,7 +30,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
res.end();
});
let { modules = [], history = [], prompt, variable = {} } = req.body as Props;
let { modules = [], history = [], prompt, variables = {} } = req.body as Props;
try {
if (!history || !modules || !prompt) {
@@ -48,9 +49,9 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
const { responseData } = await dispatchModules({
res,
modules: modules,
variable,
variables,
params: {
history,
history: gptMessage2ChatType(history),
userChatInput: prompt
},
stream: true

View File

@@ -5,12 +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 { historyId, contentId } = req.query as { historyId: string; contentId: string };
console.log(historyId, contentId);
if (!chatId || !contentId) {
if (!historyId || !contentId) {
throw new Error('缺少参数');
}
@@ -19,7 +17,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
// 凭证校验
const { userId } = await authUser({ req, authToken: true });
const chatRecord = await Chat.findById(chatId);
const chatRecord = await Chat.findById(historyId);
if (!chatRecord) {
throw new Error('找不到对话');
@@ -28,7 +26,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
// 删除一条数据库记录
await Chat.updateOne(
{
_id: chatId,
_id: historyId,
userId
},
{ $pull: { content: { _id: contentId } } }

View File

@@ -2,31 +2,32 @@ import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { connectToDatabase, Chat } from '@/service/mongo';
import { authUser } from '@/service/utils/auth';
import type { HistoryItemType } from '@/types/chat';
import type { ChatHistoryItemType } from '@/types/chat';
/* 获取历史记录 */
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try {
const { appId } = req.body as { appId?: string };
const { userId } = await authUser({ req, authToken: true });
await connectToDatabase();
const data = await Chat.find(
{
userId
userId,
...(appId && { appId })
},
'_id title top customTitle modelId updateTime latestChat'
'_id title top customTitle appId updateTime'
)
.sort({ top: -1, updateTime: -1 })
.limit(20);
jsonRes<HistoryItemType[]>(res, {
jsonRes<ChatHistoryItemType[]>(res, {
data: data.map((item) => ({
_id: item._id,
updateTime: item.updateTime,
modelId: item.modelId,
appId: item.appId,
title: item.customTitle || item.title,
latestChat: item.latestChat,
top: item.top
}))
});

View File

@@ -4,7 +4,7 @@ import { connectToDatabase, Chat } from '@/service/mongo';
import { authUser } from '@/service/utils/auth';
export type Props = {
chatId: '' | string;
historyId: string;
customTitle?: string;
top?: boolean;
};
@@ -12,7 +12,7 @@ export type Props = {
/* 更新聊天标题 */
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try {
const { chatId, customTitle, top } = req.body as Props;
const { historyId, customTitle, top } = req.body as Props;
const { userId } = await authUser({ req, authToken: true });
@@ -20,7 +20,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
await Chat.findOneAndUpdate(
{
_id: chatId,
_id: historyId,
userId
},
{

View File

@@ -6,23 +6,25 @@ import { authUser } from '@/service/utils/auth';
import { ChatItemType } from '@/types/chat';
import { authApp } from '@/service/utils/auth';
import mongoose from 'mongoose';
import type { AppSchema } from '@/types/mongoSchema';
import type { AppSchema, ChatSchema } from '@/types/mongoSchema';
import { FlowModuleTypeEnum } from '@/constants/flow';
import { SystemInputEnum } from '@/constants/app';
/* 初始化我的聊天框,需要身份验证 */
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 { appId, historyId } = req.query as {
appId: '' | string;
historyId: '' | string;
};
await connectToDatabase();
// 没有 modelId 时直接获取用户的第一个id
// 没有 appId 时直接获取用户的第一个id
const app = await (async () => {
if (!modelId) {
if (!appId) {
const myModel = await App.findOne({ userId });
if (!myModel) {
const { _id } = await App.create({
@@ -36,7 +38,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
} else {
// 校验使用权限
const authRes = await authApp({
appId: modelId,
appId,
userId,
authUser: false,
authOwner: false
@@ -45,63 +47,71 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
}
})();
modelId = modelId || app._id;
appId = appId || app._id;
// 历史记录
let history: ChatItemType[] = [];
if (chatId) {
// auth chatId
const chat = await Chat.countDocuments({
_id: chatId,
userId
});
if (chat === 0) {
throw new Error('聊天框不存在');
}
// 获取 chat.content 数据
history = await Chat.aggregate([
{
$match: {
_id: new mongoose.Types.ObjectId(chatId),
userId: new mongoose.Types.ObjectId(userId)
const { chat, history = [] }: { chat?: ChatSchema; history?: ChatItemType[] } =
await (async () => {
if (historyId) {
// auth chatId
const chat = await Chat.findOne({
_id: historyId,
userId
});
if (!chat) {
throw new Error('聊天框不存在');
}
},
{
$project: {
content: {
$slice: ['$content', -50] // 返回 content 数组的最后50个元素
// 获取 chat.content 数据
const history = await Chat.aggregate([
{
$match: {
_id: new mongoose.Types.ObjectId(historyId),
userId: new mongoose.Types.ObjectId(userId)
}
},
{
$project: {
content: {
$slice: ['$content', -50] // 返回 content 数组的最后50个元素
}
}
},
{ $unwind: '$content' },
{
$project: {
_id: '$content._id',
obj: '$content.obj',
value: '$content.value',
systemPrompt: '$content.systemPrompt',
quoteLen: { $size: { $ifNull: ['$content.quote', []] } }
}
}
}
},
{ $unwind: '$content' },
{
$project: {
_id: '$content._id',
obj: '$content.obj',
value: '$content.value',
systemPrompt: '$content.systemPrompt',
quoteLen: { $size: { $ifNull: ['$content.quote', []] } }
}
]);
return { history, chat };
}
]);
}
return {};
})();
const isOwner = String(app.userId) === userId;
jsonRes<InitChatResponse>(res, {
data: {
chatId: chatId || '',
modelId: modelId,
model: {
historyId,
appId,
app: {
variableModules: app.modules
.find((item) => item.flowType === FlowModuleTypeEnum.userGuide)
?.inputs?.find((item) => item.key === SystemInputEnum.variables)?.value,
welcomeText: app.modules
.find((item) => item.flowType === FlowModuleTypeEnum.userGuide)
?.inputs?.find((item) => item.key === SystemInputEnum.welcomeText)?.value,
name: app.name,
avatar: app.avatar,
intro: app.intro,
canUse: app.share.isShare || isOwner
},
chatModel: app.chat.chatModel,
systemPrompt: isOwner ? app.chat.systemPrompt : '',
limitPrompt: isOwner ? app.chat.limitPrompt : '',
title: chat?.title || '新对话',
variables: chat?.variables || {},
history
}
});

View File

@@ -7,15 +7,16 @@ import { authUser } from '@/service/utils/auth';
import { Types } from 'mongoose';
type Props = {
chatId?: string;
modelId: string;
historyId?: string;
appId: string;
variables?: Record<string, any>;
prompts: [ChatItemType, ChatItemType];
};
/* 聊天内容存存储 */
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try {
const { chatId, modelId, prompts } = req.body as Props;
const { historyId, appId, prompts } = req.body as Props;
if (!prompts) {
throw new Error('缺少参数');
@@ -24,8 +25,8 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
const { userId } = await authUser({ req, authToken: true });
const response = await saveChat({
chatId,
modelId,
historyId,
appId,
prompts,
userId
});
@@ -42,14 +43,15 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
}
export async function saveChat({
newChatId,
chatId,
modelId,
newHistoryId,
historyId,
appId,
prompts,
variables,
userId
}: Props & { newChatId?: Types.ObjectId; userId: string }): Promise<{ newChatId: string }> {
}: Props & { newHistoryId?: Types.ObjectId; userId: string }): Promise<{ newHistoryId: string }> {
await connectToDatabase();
const { app } = await authApp({ appId: modelId, userId, authOwner: false });
const { app } = await authApp({ appId, userId, authOwner: false });
const content = prompts.map((item) => ({
_id: item._id,
@@ -60,43 +62,45 @@ export async function saveChat({
}));
if (String(app.userId) === userId) {
await App.findByIdAndUpdate(modelId, {
await App.findByIdAndUpdate(appId, {
updateTime: new Date()
});
}
const [response] = await Promise.all([
...(chatId
...(historyId
? [
Chat.findByIdAndUpdate(chatId, {
Chat.findByIdAndUpdate(historyId, {
$push: {
content: {
$each: content
}
},
variables,
title: content[0].value.slice(0, 20),
latestChat: content[1].value,
updateTime: new Date()
}).then(() => ({
newChatId: ''
newHistoryId: ''
}))
]
: [
Chat.create({
_id: newChatId,
_id: newHistoryId,
userId,
modelId,
appId,
variables,
content,
title: content[0].value.slice(0, 20),
latestChat: content[1].value
}).then((res) => ({
newChatId: String(res._id)
newHistoryId: String(res._id)
}))
]),
// update app
...(String(app.userId) === userId
? [
App.findByIdAndUpdate(modelId, {
App.findByIdAndUpdate(appId, {
updateTime: new Date()
})
]
@@ -105,6 +109,6 @@ export async function saveChat({
return {
// @ts-ignore
newChatId: response?.newChatId || ''
newHistoryId: response?.newHistoryId || ''
};
}

View File

@@ -1,189 +0,0 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { connectToDatabase } from '@/service/mongo';
import { authUser, authApp, getApiKey } from '@/service/utils/auth';
import { modelServiceToolMap, resStreamResponse } from '@/service/utils/chat';
import { ChatItemType } from '@/types/chat';
import { jsonRes } from '@/service/response';
import { ChatModelMap } from '@/constants/model';
import { pushChatBill } from '@/service/events/pushBill';
import { ChatRoleEnum } from '@/constants/chat';
import { withNextCors } from '@/service/utils/tools';
import { BillTypeEnum } from '@/constants/user';
import { appKbSearch } from '../kb/appKbSearch';
/* 发送提示词 */
export default withNextCors(async function handler(req: NextApiRequest, res: NextApiResponse) {
res.on('close', () => {
res.end();
});
res.on('error', () => {
console.log('error: ', 'request error');
res.end();
});
try {
const {
chatId,
prompts,
modelId,
isStream = true
} = req.body as {
chatId?: string;
prompts: ChatItemType[];
modelId: string;
isStream: boolean;
};
if (!prompts || !modelId) {
throw new Error('缺少参数');
}
if (!Array.isArray(prompts)) {
throw new Error('prompts is not array');
}
if (prompts.length > 30 || prompts.length === 0) {
throw new Error('Prompts arr length range 1-30');
}
await connectToDatabase();
let startTime = Date.now();
/* 凭证校验 */
const { userId } = await authUser({ req });
const { app } = await authApp({
userId,
appId: modelId
});
/* get api key */
const { systemAuthKey: apiKey } = await getApiKey({
model: app.chat.chatModel,
userId,
mustPay: true
});
const modelConstantsData = ChatModelMap[app.chat.chatModel];
const prompt = prompts[prompts.length - 1];
const {
userSystemPrompt = [],
userLimitPrompt = [],
quotePrompt = []
} = await (async () => {
// 使用了知识库搜索
if (app.chat.relatedKbs?.length > 0) {
const { quotePrompt, userSystemPrompt, userLimitPrompt } = await appKbSearch({
model: app,
userId,
fixedQuote: [],
prompt: prompt,
similarity: app.chat.searchSimilarity,
limit: app.chat.searchLimit
});
return {
userSystemPrompt,
userLimitPrompt,
quotePrompt: [quotePrompt]
};
}
return {
userSystemPrompt: app.chat.systemPrompt
? [
{
obj: ChatRoleEnum.System,
value: app.chat.systemPrompt
}
]
: [],
userLimitPrompt: app.chat.limitPrompt
? [
{
obj: ChatRoleEnum.Human,
value: app.chat.limitPrompt
}
]
: []
};
})();
// search result is empty
if (app.chat.relatedKbs?.length > 0 && !quotePrompt[0]?.value && app.chat.searchEmptyText) {
const response = app.chat.searchEmptyText;
return res.end(response);
}
// 读取对话内容
const completePrompts = [
...quotePrompt,
...userSystemPrompt,
...prompts.slice(0, -1),
...userLimitPrompt,
prompt
];
// 计算温度
const temperature = (modelConstantsData.maxTemperature * (app.chat.temperature / 10)).toFixed(
2
);
// 发出请求
const { streamResponse, responseMessages, responseText, totalTokens } =
await modelServiceToolMap.chatCompletion({
model: app.chat.chatModel,
apiKey,
temperature: +temperature,
messages: completePrompts,
stream: isStream,
res
});
console.log('api response time:', `${(Date.now() - startTime) / 1000}s`);
if (res.closed) return res.end();
const { textLen = 0, tokens = totalTokens } = await (async () => {
if (isStream) {
try {
const { finishMessages, totalTokens } = await resStreamResponse({
model: app.chat.chatModel,
res,
chatResponse: streamResponse,
prompts: responseMessages
});
res.end();
return {
textLen: finishMessages.map((item) => item.value).join('').length,
tokens: totalTokens
};
} catch (error) {
res.end();
console.log('error结束', error);
}
} else {
jsonRes(res, {
data: responseText
});
return {
textLen: responseMessages.map((item) => item.value).join('').length
};
}
return {};
})();
pushChatBill({
isPay: true,
chatModel: app.chat.chatModel,
userId,
textLen,
tokens,
type: BillTypeEnum.openapiChat
});
} catch (err: any) {
res.status(500);
jsonRes(res, {
code: 500,
error: err
});
}
});

View File

@@ -1,43 +1,41 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { connectToDatabase } from '@/service/mongo';
import { authUser, authApp, getApiKey, authShareChat } from '@/service/utils/auth';
import { modelServiceToolMap, V2_StreamResponse } from '@/service/utils/chat';
import { jsonRes } from '@/service/response';
import { ChatModelMap } from '@/constants/model';
import { pushChatBill, updateShareChatBill } from '@/service/events/pushBill';
import { authUser, authApp, authShareChat } from '@/service/utils/auth';
import { sseErrRes, jsonRes } from '@/service/response';
import { ChatRoleEnum, sseResponseEventEnum } from '@/constants/chat';
import { withNextCors } from '@/service/utils/tools';
import { BillTypeEnum } from '@/constants/user';
import { appKbSearch } from '../../../openapi/kb/appKbSearch';
import type { CreateChatCompletionRequest } from 'openai';
import { gptMessage2ChatType, textAdaptGptResponse } from '@/utils/adapt';
import { getChatHistory } from './getHistory';
import { saveChat } from '@/pages/api/chat/saveChat';
import { sseResponse } from '@/service/utils/tools';
import { type ChatCompletionRequestMessage } from 'openai';
import { SpecificInputEnum, AppModuleItemTypeEnum } from '@/constants/app';
import { Types } from 'mongoose';
import { sensitiveCheck } from '../../text/sensitiveCheck';
import { moduleFetch } from '@/service/api/request';
import { AppModuleItemType, RunningModuleItemType } from '@/types/app';
import { FlowInputItemTypeEnum } from '@/constants/flow';
export type MessageItemType = ChatCompletionRequestMessage & { _id?: string };
type FastGptWebChatProps = {
chatId?: string; // undefined: nonuse history, '': new chat, 'xxxxx': use history
historyId?: string; // undefined: nonuse history, '': new chat, 'xxxxx': use history
appId?: string;
};
type FastGptShareChatProps = {
password?: string;
shareId?: string;
};
export type Props = CreateChatCompletionRequest &
FastGptWebChatProps &
FastGptShareChatProps & {
messages: MessageItemType[];
stream?: boolean;
variables: Record<string, any>;
};
export type ChatResponseType = {
newChatId: string;
quoteLen?: number;
};
/* 发送提示词 */
export default withNextCors(async function handler(req: NextApiRequest, res: NextApiResponse) {
res.on('close', () => {
res.end();
@@ -47,8 +45,14 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex
res.end();
});
let { chatId, appId, shareId, password = '', stream = false, messages = [] } = req.body as Props;
let step = 0;
let {
historyId,
appId,
shareId,
stream = false,
messages = [],
variables = {}
} = req.body as Props;
try {
if (!messages) {
@@ -68,8 +72,7 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex
authType
} = await (shareId
? authShareChat({
shareId,
password
shareId
})
: authUser({ req }));
@@ -78,257 +81,96 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex
throw new Error('appId is empty');
}
// auth app permission
const { app, showModelDetail } = await authApp({
userId,
appId,
authOwner: false,
reserveDetail: true
});
// auth app, get history
const [{ app }, { history }] = await Promise.all([
authApp({
appId,
userId
}),
getChatHistory({ historyId, userId })
]);
const showAppDetail = !shareId && showModelDetail;
/* get api key */
const { systemAuthKey: apiKey, userOpenAiKey } = await getApiKey({
model: app.chat.chatModel,
userId,
mustPay: authType !== 'token'
});
// get history
const { history } = await getChatHistory({ chatId, userId });
const prompts = history.concat(gptMessage2ChatType(messages));
// adapt fastgpt web
if (prompts[prompts.length - 1].obj === 'AI') {
prompts.pop();
}
// user question
const prompt = prompts[prompts.length - 1];
const prompt = prompts.pop();
const {
rawSearch = [],
userSystemPrompt = [],
userLimitPrompt = [],
quotePrompt = []
} = await (async () => {
// 使用了知识库搜索
if (app.chat.relatedKbs?.length > 0) {
const { rawSearch, quotePrompt, userSystemPrompt, userLimitPrompt } = await appKbSearch({
model: app,
userId,
fixedQuote: history[history.length - 1]?.quote,
prompt,
similarity: app.chat.searchSimilarity,
limit: app.chat.searchLimit
});
return {
rawSearch,
userSystemPrompt,
userLimitPrompt,
quotePrompt: [quotePrompt]
};
}
return {
userSystemPrompt: app.chat.systemPrompt
? [
{
obj: ChatRoleEnum.System,
value: app.chat.systemPrompt
}
]
: [],
userLimitPrompt: app.chat.limitPrompt
? [
{
obj: ChatRoleEnum.Human,
value: app.chat.limitPrompt
}
]
: []
};
})();
// search result is empty
if (app.chat.relatedKbs?.length > 0 && !quotePrompt[0]?.value && app.chat.searchEmptyText) {
const response = app.chat.searchEmptyText;
if (stream) {
sseResponse({
res,
event: sseResponseEventEnum.answer,
data: textAdaptGptResponse({
text: response,
model: app.chat.chatModel,
finish_reason: 'stop'
})
});
return res.end();
} else {
return res.json({
id: chatId || '',
object: 'chat.completion',
created: 1688608930,
model: app.chat.chatModel,
usage: { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 },
choices: [
{ message: { role: 'assistant', content: response }, finish_reason: 'stop', index: 0 }
]
});
}
if (!prompt) {
throw new Error('Question is empty');
}
// api messages. [quote,context,systemPrompt,question]
const completePrompts = [
...quotePrompt,
...userSystemPrompt,
...prompts.slice(0, -1),
...userLimitPrompt,
prompt
];
// chat temperature
const modelConstantsData = ChatModelMap[app.chat.chatModel];
// FastGpt temperature range: 1~10
const temperature = (modelConstantsData.maxTemperature * (app.chat.temperature / 10)).toFixed(
2
);
const newHistoryId = historyId === '' ? new Types.ObjectId() : undefined;
if (stream && newHistoryId) {
res.setHeader('newHistoryId', String(newHistoryId));
}
await sensitiveCheck({
input: `${userSystemPrompt[0]?.value}\n${userLimitPrompt[0]?.value}\n${prompt.value}`
/* start process */
const { responseData, answerText } = await dispatchModules({
res,
modules: app.modules,
variables,
params: {
history: prompts,
userChatInput: prompt.value
},
stream
});
// start app api. responseText and totalTokens: valid only if stream = false
const { streamResponse, responseMessages, responseText, totalTokens } =
await modelServiceToolMap.chatCompletion({
model: app.chat.chatModel,
apiKey: userOpenAiKey || apiKey,
temperature: +temperature,
maxToken: app.chat.maxToken,
messages: completePrompts,
stream,
res
});
console.log('api response time:', `${(Date.now() - startTime) / 1000}s`);
if (res.closed) return res.end();
// create a chatId
const newChatId = chatId === '' ? new Types.ObjectId() : undefined;
// response answer
const {
textLen = 0,
answer = responseText,
tokens = totalTokens
} = await (async () => {
if (stream) {
// 创建响应流
res.setHeader('Content-Type', 'text/event-stream;charset=utf-8');
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Transfer-Encoding', 'chunked');
res.setHeader('X-Accel-Buffering', 'no');
res.setHeader('Cache-Control', 'no-cache, no-transform');
step = 1;
try {
// response newChatId and quota
sseResponse({
res,
event: sseResponseEventEnum.chatResponse,
data: JSON.stringify({
newChatId,
quoteLen: rawSearch.length
})
});
// response answer
const { finishMessages, totalTokens, responseContent } = await V2_StreamResponse({
model: app.chat.chatModel,
res,
chatResponse: streamResponse,
prompts: responseMessages
});
return {
answer: responseContent,
textLen: finishMessages.map((item) => item.value).join('').length,
tokens: totalTokens
};
} catch (error) {
return Promise.reject(error);
}
} else {
return {
textLen: responseMessages.map((item) => item.value).join('').length
};
}
})();
// save chat history
if (typeof chatId === 'string') {
// save chat
if (typeof historyId === 'string') {
await saveChat({
newChatId,
chatId,
modelId: appId,
historyId,
newHistoryId,
appId,
prompts: [
prompt,
{
_id: messages[messages.length - 1]._id,
obj: ChatRoleEnum.AI,
value: answer,
...(showAppDetail
? {
quote: rawSearch,
systemPrompt: `${userSystemPrompt[0]?.value}\n\n${userLimitPrompt[0]?.value}`
}
: {})
value: answerText,
responseData
}
],
userId
});
}
// close response
if (stream) {
sseResponse({
res,
event: sseResponseEventEnum.answer,
data: '[DONE]'
});
sseResponse({
res,
event: sseResponseEventEnum.appStreamResponse,
data: JSON.stringify(responseData)
});
res.end();
} else {
res.json({
...(showAppDetail
? {
rawSearch
}
: {}),
newChatId,
id: chatId || '',
object: 'chat.completion',
created: 1688608930,
model: app.chat.chatModel,
usage: { prompt_tokens: 0, completion_tokens: 0, total_tokens: tokens },
data: {
newHistoryId,
...responseData
},
id: historyId || '',
model: '',
usage: { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 },
choices: [
{ message: { role: 'assistant', content: answer }, finish_reason: 'stop', index: 0 }
{
message: [{ role: 'assistant', content: answerText }],
finish_reason: 'stop',
index: 0
}
]
});
}
pushChatBill({
isPay: !userOpenAiKey,
chatModel: app.chat.chatModel,
userId,
textLen,
tokens,
type: authType === 'apikey' ? BillTypeEnum.openapiChat : BillTypeEnum.chat
});
shareId &&
updateShareChatBill({
shareId,
tokens
});
} catch (err: any) {
res.status(500);
if (step === 1) {
sseResponse({
res,
event: sseResponseEventEnum.error,
data: JSON.stringify(err)
});
if (stream) {
res.status(500);
sseErrRes(res, err);
res.end();
} else {
jsonRes(res, {
@@ -338,3 +180,232 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex
}
}
});
export async function dispatchModules({
res,
modules,
params = {},
variables = {},
stream = false
}: {
res: NextApiResponse;
modules: AppModuleItemType[];
params?: Record<string, any>;
variables?: Record<string, any>;
stream?: boolean;
}) {
const runningModules = loadModules(modules, variables);
let storeData: Record<string, any> = {};
let responseData: Record<string, any> = {};
let answerText = '';
function pushStore({
isResponse = false,
answer,
data = {}
}: {
isResponse?: boolean;
answer?: string;
data?: Record<string, any>;
}) {
if (isResponse) {
responseData = {
...responseData,
...data
};
}
if (answer) {
answerText += answer;
}
storeData = {
...storeData,
...data
};
}
function moduleInput(
module: RunningModuleItemType,
data: Record<string, any> = {}
): Promise<any> {
const checkInputFinish = () => {
return !module.inputs.find((item: any) => item.value === undefined);
};
const updateInputValue = (key: string, value: any) => {
const index = module.inputs.findIndex((item: any) => item.key === key);
if (index === -1) return;
module.inputs[index].value = value;
};
const set = new Set();
return Promise.all(
Object.entries(data).map(([key, val]: any) => {
updateInputValue(key, val);
if (!set.has(module.moduleId) && checkInputFinish()) {
set.add(module.moduleId);
return moduleRun(module);
}
})
);
}
function moduleOutput(
module: RunningModuleItemType,
result: Record<string, any> = {}
): Promise<any> {
return Promise.all(
module.outputs.map((outputItem) => {
if (result[outputItem.key] === undefined) return;
/* update output value */
outputItem.value = result[outputItem.key];
pushStore({
isResponse: outputItem.response,
answer: outputItem.answer ? outputItem.value : '',
data: {
[outputItem.key]: outputItem.value
}
});
/* update target */
return Promise.all(
outputItem.targets.map((target: any) => {
// find module
const targetModule = runningModules.find((item) => item.moduleId === target.moduleId);
if (!targetModule) return;
return moduleInput(targetModule, { [target.key]: outputItem.value });
})
);
})
);
}
async function moduleRun(module: RunningModuleItemType): Promise<any> {
if (res.closed) return Promise.resolve();
console.log('run=========', module.type, module.url);
// direct answer
if (module.type === AppModuleItemTypeEnum.answer) {
const text =
module.inputs.find((item) => item.key === SpecificInputEnum.answerText)?.value || '';
pushStore({
answer: text
});
return StreamAnswer({
res,
stream,
text: text
});
}
if (module.type === AppModuleItemTypeEnum.switch) {
return moduleOutput(module, switchResponse(module));
}
if (
(module.type === AppModuleItemTypeEnum.http ||
module.type === AppModuleItemTypeEnum.initInput) &&
module.url
) {
// get fetch params
const params: Record<string, any> = {};
module.inputs.forEach((item: any) => {
params[item.key] = item.value;
});
const data = {
stream,
...params
};
// response data
const fetchRes = await moduleFetch({
res,
url: module.url,
data
});
return moduleOutput(module, fetchRes);
}
}
// start process width initInput
const initModules = runningModules.filter(
(item) => item.type === AppModuleItemTypeEnum.initInput
);
await Promise.all(initModules.map((module) => moduleInput(module, params)));
return {
responseData,
answerText
};
}
function loadModules(
modules: AppModuleItemType[],
variables: Record<string, any>
): RunningModuleItemType[] {
return modules.map((module) => {
return {
moduleId: module.moduleId,
type: module.type,
url: module.url,
inputs: module.inputs
.filter((item) => item.type !== FlowInputItemTypeEnum.target || item.connected) // filter unconnected target input
.map((item) => {
if (typeof item.value !== 'string') {
return {
key: item.key,
value: item.value
};
}
// variables replace
const replacedVal = item.value.replace(
/{{(.*?)}}/g,
(match, key) => variables[key.trim()] || match
);
return {
key: item.key,
value: replacedVal
};
}),
outputs: module.outputs.map((item) => ({
key: item.key,
answer: item.key === SpecificInputEnum.answerText,
response: item.response,
value: undefined,
targets: item.targets
}))
};
});
}
function StreamAnswer({
res,
stream = false,
text = ''
}: {
res: NextApiResponse;
stream?: boolean;
text?: string;
}) {
if (stream && text) {
return sseResponse({
res,
event: sseResponseEventEnum.answer,
data: textAdaptGptResponse({
text: text.replace(/\\n/g, '\n')
})
});
}
return text;
}
function switchResponse(module: RunningModuleItemType) {
const val = module?.inputs?.[0]?.value;
if (val) {
return { true: 1 };
}
return { false: 1 };
}

View File

@@ -1,405 +0,0 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { connectToDatabase } from '@/service/mongo';
import { authUser, authApp, getApiKey, authShareChat } from '@/service/utils/auth';
import { sseErrRes, jsonRes } from '@/service/response';
import { ChatRoleEnum, sseResponseEventEnum } from '@/constants/chat';
import { withNextCors } from '@/service/utils/tools';
import type { CreateChatCompletionRequest } from 'openai';
import { gptMessage2ChatType, textAdaptGptResponse } from '@/utils/adapt';
import { getChatHistory } from './getHistory';
import { saveChat } from '@/pages/api/chat/saveChat';
import { sseResponse } from '@/service/utils/tools';
import { type ChatCompletionRequestMessage } from 'openai';
import { SpecificInputEnum, AppModuleItemTypeEnum } from '@/constants/app';
import { model, Types } from 'mongoose';
import { moduleFetch } from '@/service/api/request';
import { AppModuleItemType, RunningModuleItemType } from '@/types/app';
import { FlowInputItemTypeEnum, FlowOutputItemTypeEnum } from '@/constants/flow';
import { SystemInputEnum } from '@/constants/app';
export type MessageItemType = ChatCompletionRequestMessage & { _id?: string };
type FastGptWebChatProps = {
chatId?: string; // undefined: nonuse history, '': new chat, 'xxxxx': use history
appId?: string;
};
type FastGptShareChatProps = {
shareId?: string;
};
export type Props = CreateChatCompletionRequest &
FastGptWebChatProps &
FastGptShareChatProps & {
messages: MessageItemType[];
stream?: boolean;
variables: Record<string, any>;
};
export type ChatResponseType = {
newChatId: string;
quoteLen?: number;
};
export default withNextCors(async function handler(req: NextApiRequest, res: NextApiResponse) {
res.on('close', () => {
res.end();
});
res.on('error', () => {
console.log('error: ', 'request error');
res.end();
});
let { chatId, appId, shareId, stream = false, messages = [], variables = {} } = req.body as Props;
try {
if (!messages) {
throw new Error('Prams Error');
}
if (!Array.isArray(messages)) {
throw new Error('messages is not array');
}
await connectToDatabase();
let startTime = Date.now();
/* user auth */
const {
userId,
appId: authAppid,
authType
} = await (shareId
? authShareChat({
shareId
})
: authUser({ req }));
appId = appId ? appId : authAppid;
if (!appId) {
throw new Error('appId is empty');
}
// auth app, get history
const [{ app }, { history }] = await Promise.all([
authApp({
appId,
userId
}),
getChatHistory({ chatId, userId })
]);
const prompts = history.concat(gptMessage2ChatType(messages));
if (prompts[prompts.length - 1].obj === 'AI') {
prompts.pop();
}
// user question
const prompt = prompts.pop();
if (!prompt) {
throw new Error('Question is empty');
}
const newChatId = chatId === '' ? new Types.ObjectId() : undefined;
if (stream && newChatId) {
res.setHeader('newChatId', String(newChatId));
}
/* start process */
const { responseData, answerText } = await dispatchModules({
res,
modules: app.modules,
variables,
params: {
history: prompts,
userChatInput: prompt.value
},
stream
});
// save chat
if (typeof chatId === 'string') {
await saveChat({
chatId,
newChatId,
modelId: appId,
prompts: [
prompt,
{
_id: messages[messages.length - 1]._id,
obj: ChatRoleEnum.AI,
value: answerText,
responseData
}
],
userId
});
}
if (stream) {
sseResponse({
res,
event: sseResponseEventEnum.answer,
data: '[DONE]'
});
sseResponse({
res,
event: sseResponseEventEnum.appStreamResponse,
data: JSON.stringify(responseData)
});
res.end();
} else {
res.json({
data: {
newChatId,
...responseData
},
id: chatId || '',
model: '',
usage: { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 },
choices: [
{
message: [{ role: 'assistant', content: answerText }],
finish_reason: 'stop',
index: 0
}
]
});
}
} catch (err: any) {
if (stream) {
res.status(500);
sseErrRes(res, err);
res.end();
} else {
jsonRes(res, {
code: 500,
error: err
});
}
}
});
export async function dispatchModules({
res,
modules,
params = {},
variables = {},
stream = false
}: {
res: NextApiResponse;
modules: AppModuleItemType[];
params?: Record<string, any>;
variables?: Record<string, any>;
stream?: boolean;
}) {
const runningModules = loadModules(modules, variables);
let storeData: Record<string, any> = {};
let responseData: Record<string, any> = {};
let answerText = '';
function pushStore({
isResponse = false,
answer,
data = {}
}: {
isResponse?: boolean;
answer?: string;
data?: Record<string, any>;
}) {
if (isResponse) {
responseData = {
...responseData,
...data
};
}
if (answer) {
answerText += answer;
}
storeData = {
...storeData,
...data
};
}
function moduleInput(
module: RunningModuleItemType,
data: Record<string, any> = {}
): Promise<any> {
const checkInputFinish = () => {
return !module.inputs.find((item: any) => item.value === undefined);
};
const updateInputValue = (key: string, value: any) => {
const index = module.inputs.findIndex((item: any) => item.key === key);
if (index === -1) return;
module.inputs[index].value = value;
};
const set = new Set();
return Promise.all(
Object.entries(data).map(([key, val]: any) => {
updateInputValue(key, val);
if (!set.has(module.moduleId) && checkInputFinish()) {
set.add(module.moduleId);
return moduleRun(module);
}
})
);
}
function moduleOutput(
module: RunningModuleItemType,
result: Record<string, any> = {}
): Promise<any> {
return Promise.all(
module.outputs.map((outputItem) => {
if (result[outputItem.key] === undefined) return;
/* update output value */
outputItem.value = result[outputItem.key];
pushStore({
isResponse: outputItem.response,
answer: outputItem.answer ? outputItem.value : '',
data: {
[outputItem.key]: outputItem.value
}
});
/* update target */
return Promise.all(
outputItem.targets.map((target: any) => {
// find module
const targetModule = runningModules.find((item) => item.moduleId === target.moduleId);
if (!targetModule) return;
return moduleInput(targetModule, { [target.key]: outputItem.value });
})
);
})
);
}
async function moduleRun(module: RunningModuleItemType): Promise<any> {
if (res.closed) return Promise.resolve();
console.log('run=========', module.type, module.url);
// direct answer
if (module.type === AppModuleItemTypeEnum.answer) {
const text =
module.inputs.find((item) => item.key === SpecificInputEnum.answerText)?.value || '';
pushStore({
answer: text
});
return StreamAnswer({
res,
stream,
text: text
});
}
if (module.type === AppModuleItemTypeEnum.switch) {
return moduleOutput(module, switchResponse(module));
}
if (
(module.type === AppModuleItemTypeEnum.http ||
module.type === AppModuleItemTypeEnum.initInput) &&
module.url
) {
// get fetch params
const params: Record<string, any> = {};
module.inputs.forEach((item: any) => {
params[item.key] = item.value;
});
const data = {
stream,
...params
};
// response data
const fetchRes = await moduleFetch({
res,
url: module.url,
data
});
return moduleOutput(module, fetchRes);
}
}
// start process width initInput
const initModules = runningModules.filter(
(item) => item.type === AppModuleItemTypeEnum.initInput
);
await Promise.all(initModules.map((module) => moduleInput(module, params)));
return {
responseData,
answerText
};
}
function loadModules(
modules: AppModuleItemType[],
variables: Record<string, any>
): RunningModuleItemType[] {
return modules.map((module) => {
return {
moduleId: module.moduleId,
type: module.type,
url: module.url,
inputs: module.inputs
.filter((item) => item.type !== FlowInputItemTypeEnum.target || item.connected) // filter unconnected target input
.map((item) => {
if (typeof item.value !== 'string') {
return {
key: item.key,
value: item.value
};
}
// variables replace
const replacedVal = item.value.replace(
/{{(.*?)}}/g,
(match, key) => variables[key.trim()] || match
);
return {
key: item.key,
value: replacedVal
};
}),
outputs: module.outputs.map((item) => ({
key: item.key,
answer: item.key === SpecificInputEnum.answerText,
response: item.response,
value: undefined,
targets: item.targets
}))
};
});
}
function StreamAnswer({
res,
stream = false,
text = ''
}: {
res: NextApiResponse;
stream?: boolean;
text?: string;
}) {
if (stream && text) {
return sseResponse({
res,
event: sseResponseEventEnum.answer,
data: textAdaptGptResponse({
text: text.replace(/\\n/g, '\n')
})
});
}
return text;
}
function switchResponse(module: RunningModuleItemType) {
const val = module?.inputs?.[0]?.value;
if (val) {
return { true: 1 };
}
return { false: 1 };
}

View File

@@ -7,7 +7,7 @@ import { Types } from 'mongoose';
import type { ChatItemType } from '@/types/chat';
export type Props = {
chatId?: string;
historyId?: string;
limit?: number;
};
export type Response = { history: ChatItemType[] };
@@ -16,11 +16,11 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
try {
await connectToDatabase();
const { userId } = await authUser({ req });
const { chatId, limit } = req.body as Props;
const { historyId, limit } = req.body as Props;
jsonRes<Response>(res, {
data: await getChatHistory({
chatId,
historyId,
userId,
limit
})
@@ -34,16 +34,16 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
}
export async function getChatHistory({
chatId,
historyId,
userId,
limit = 50
}: Props & { userId: string }): Promise<Response> {
if (!chatId) {
if (!historyId) {
return { history: [] };
}
const history = await Chat.aggregate([
{ $match: { _id: new Types.ObjectId(chatId), userId: new Types.ObjectId(userId) } },
{ $match: { _id: new Types.ObjectId(historyId), userId: new Types.ObjectId(userId) } },
{
$project: {
content: {