* chat item table

* perf: chat item save

* docs

* limit

* docs

* docs

* perf: node card

* docs

* docs
This commit is contained in:
Archer
2023-08-17 16:57:22 +08:00
committed by GitHub
parent ce61ac3fac
commit 324e4a0e75
49 changed files with 617 additions and 359 deletions

View File

@@ -45,9 +45,7 @@ async function init(limit: number, skip: number) {
chatId: { $exists: false }
},
'_id'
)
.limit(limit)
.skip(skip);
).limit(limit);
await Promise.all(
chats.map((chat) =>

View File

@@ -0,0 +1,98 @@
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { authUser } from '@/service/utils/auth';
import { connectToDatabase, Chat, ChatItem } from '@/service/mongo';
import { customAlphabet } from 'nanoid';
const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyz1234567890', 24);
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try {
await authUser({ req, authRoot: true });
await connectToDatabase();
const { limit = 100 } = req.body as { limit: number };
let skip = 0;
const total = await Chat.countDocuments({
content: { $exists: true, $not: { $size: 0 } },
isInit: { $ne: true }
});
const totalChat = await Chat.aggregate([
{
$project: {
contentLength: { $size: '$content' }
}
},
{
$group: {
_id: null,
totalLength: { $sum: '$contentLength' }
}
}
]);
console.log('chatLen:', total, totalChat);
let promise = Promise.resolve();
for (let i = 0; i < total; i += limit) {
const skipVal = skip;
skip += limit;
promise = promise
.then(() => init(limit))
.then(() => {
console.log(skipVal);
});
}
await promise;
jsonRes(res, {});
} catch (error) {
jsonRes(res, {
code: 500,
error
});
}
}
async function init(limit: number) {
// 遍历 app
const chats = await Chat.find(
{
content: { $exists: true, $not: { $size: 0 } },
isInit: { $ne: true }
},
'_id userId appId chatId content'
)
.sort({ updateTime: -1 })
.limit(limit);
await Promise.all(
chats.map(async (chat) => {
const inserts = chat.content
.map((item) => ({
dataId: nanoid(),
chatId: chat.chatId,
userId: chat.userId,
appId: chat.appId,
obj: item.obj,
value: item.value,
responseData: item.responseData
}))
.filter((item) => item.chatId && item.userId && item.appId && item.obj && item.value);
try {
await Promise.all(inserts.map((item) => ChatItem.create(item)));
await Chat.findByIdAndUpdate(chat._id, {
isInit: true
});
} catch (error) {
console.log(error);
await ChatItem.deleteMany({ chatId: chat.chatId });
}
})
);
}

View File

@@ -408,9 +408,7 @@ async function init(limit: number, skip: number) {
// userId: '63f9a14228d2a688d8dc9e1b'
},
'_id chat'
)
.limit(limit)
.skip(skip);
).limit(limit);
return Promise.all(
apps.map(async (app) => {

View File

@@ -1,35 +1,23 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { connectToDatabase, Chat } from '@/service/mongo';
import { connectToDatabase, ChatItem } from '@/service/mongo';
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 };
if (!chatId || !contentId) {
throw new Error('缺少参数');
}
await connectToDatabase();
// 凭证校验
const { userId } = await authUser({ req, authToken: true });
const chatRecord = await Chat.findOne({ chatId });
if (!chatRecord) {
throw new Error('找不到对话');
}
// 删除一条数据库记录
await Chat.updateOne(
{
chatId,
userId
},
{ $pull: { content: { _id: contentId } } }
);
await ChatItem.deleteOne({
dataId: contentId,
chatId,
userId
});
jsonRes(res);
} catch (err) {

View File

@@ -1,57 +0,0 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { connectToDatabase, Chat } from '@/service/mongo';
import { authUser } from '@/service/utils/auth';
import { Types } from 'mongoose';
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try {
let {
chatId,
contentId,
quoteId,
sourceText = ''
} = req.body as {
chatId: string;
contentId: string;
quoteId: string;
sourceText: string;
};
await connectToDatabase();
const { userId } = await authUser({ req, authToken: true });
if (!contentId || !chatId || !quoteId) {
throw new Error('params is error');
}
await Chat.updateOne(
{
chatId,
userId: new Types.ObjectId(userId),
'content._id': new Types.ObjectId(contentId)
},
{
$set: {
'content.$.rawSearch.$[quoteElem].source': sourceText
}
},
{
arrayFilters: [
{
'quoteElem.id': quoteId
}
]
}
);
jsonRes(res, {
data: ''
});
} catch (err) {
jsonRes(res, {
code: 500,
error: err
});
}
}

View File

@@ -1,11 +1,10 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { connectToDatabase, Chat } from '@/service/mongo';
import { Chat, ChatItem } from '@/service/mongo';
import type { InitChatResponse } from '@/api/response/chat';
import { authUser } from '@/service/utils/auth';
import { ChatItemType } from '@/types/chat';
import { authApp } from '@/service/utils/auth';
import mongoose from 'mongoose';
import type { ChatSchema } from '@/types/mongoSchema';
import { getSpecialModule, getChatModelNameList } from '@/components/ChatBox/utils';
import { TaskResponseKeyEnum } from '@/constants/chat';
@@ -27,8 +26,6 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
});
}
await connectToDatabase();
// 校验使用权限
const app = (
await authApp({
@@ -39,49 +36,42 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
})
).app;
// 历史记录
// get app and history
const { chat, history = [] }: { chat?: ChatSchema; history?: ChatItemType[] } =
await (async () => {
if (chatId) {
// auth chatId
const [chat, history] = await Promise.all([
Chat.findOne({
chatId,
userId
}),
Chat.aggregate([
Chat.findOne(
{
$match: {
chatId,
userId: new mongoose.Types.ObjectId(userId)
}
chatId,
userId
},
'title variables'
),
ChatItem.find(
{
$project: {
content: {
$slice: ['$content', -30] // 返回 content 数组的最后 30 个元素
}
}
chatId,
userId
},
{ $unwind: '$content' },
{
$project: {
_id: '$content._id',
obj: '$content.obj',
value: '$content.value',
[TaskResponseKeyEnum.responseData]: `$content.${TaskResponseKeyEnum.responseData}`
}
}
])
`dataId obj value ${TaskResponseKeyEnum.responseData}`
)
.sort({ _id: -1 })
.limit(30)
]);
if (!chat) {
throw new Error('聊天框不存在');
}
return { history, chat };
history.reverse();
return { app, history, chat };
}
return {};
})();
if (!app) {
throw new Error('Auth App Error');
}
const isOwner = String(app.userId) === userId;
jsonRes<InitChatResponse>(res, {
@@ -108,3 +98,11 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
});
}
}
export const config = {
api: {
bodyParser: {
sizeLimit: '10mb'
}
}
};

View File

@@ -1,6 +1,6 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { connectToDatabase, Chat } from '@/service/mongo';
import { connectToDatabase, Chat, ChatItem } from '@/service/mongo';
import { authUser } from '@/service/utils/auth';
type Props = {
@@ -17,16 +17,28 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
await connectToDatabase();
if (chatId) {
await Chat.findOneAndRemove({
chatId,
userId
});
await Promise.all([
Chat.findOneAndRemove({
chatId,
userId
}),
ChatItem.deleteMany({
userId,
chatId
})
]);
}
if (appId) {
await Chat.deleteMany({
appId,
userId
});
await Promise.all([
Chat.deleteMany({
appId,
userId
}),
ChatItem.deleteMany({
userId,
appId
})
]);
}
jsonRes(res);

View File

@@ -28,7 +28,7 @@ import { BillSourceEnum } from '@/constants/user';
import { ChatHistoryItemResType } from '@/types/chat';
import { UserModelSchema } from '@/types/mongoSchema';
export type MessageItemType = ChatCompletionRequestMessage & { _id?: string };
export type MessageItemType = ChatCompletionRequestMessage & { dataId?: string };
type FastGptWebChatProps = {
chatId?: string; // undefined: nonuse history, '': new chat, 'xxxxx': use history
appId?: string;
@@ -172,7 +172,7 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex
content: [
prompt,
{
_id: messages[messages.length - 1]._id,
dataId: messages[messages.length - 1].dataId,
obj: ChatRoleEnum.AI,
value: answerText,
responseData

View File

@@ -2,10 +2,9 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { authUser } from '@/service/utils/auth';
import { connectToDatabase, Chat } from '@/service/mongo';
import { connectToDatabase, ChatItem } from '@/service/mongo';
import { Types } from 'mongoose';
import type { ChatItemType } from '@/types/chat';
import { TaskResponseKeyEnum } from '@/constants/chat';
export type Props = {
chatId?: string;
@@ -37,30 +36,37 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
export async function getChatHistory({
chatId,
userId,
limit = 20
limit = 30
}: Props & { userId: string }): Promise<Response> {
if (!chatId) {
return { history: [] };
}
const history = await Chat.aggregate([
{ $match: { chatId, userId: new Types.ObjectId(userId) } },
const history = await ChatItem.aggregate([
{
$project: {
content: {
$slice: ['$content', -limit] // 返回 content 数组的最后20个元素
}
$match: {
chatId,
userId: new Types.ObjectId(userId)
}
},
{ $unwind: '$content' },
{
$sort: {
_id: -1
}
},
{
$limit: limit
},
{
$project: {
obj: '$content.obj',
value: '$content.value',
[TaskResponseKeyEnum.responseData]: `$content.responseData`
dataId: 1,
obj: 1,
value: 1
}
}
]);
history.reverse();
return { history };
}

View File

@@ -2,15 +2,13 @@ import React from 'react';
import { NodeProps } from 'reactflow';
import NodeCard from '../modules/NodeCard';
import { FlowModuleItemType } from '@/types/flow';
import Divider from '../modules/Divider';
import Container from '../modules/Container';
import RenderInput from '../render/RenderInput';
const NodeAnswer = ({
data: { moduleId, inputs, outputs, onChangeNode, ...props }
}: NodeProps<FlowModuleItemType>) => {
const NodeAnswer = ({ data }: NodeProps<FlowModuleItemType>) => {
const { moduleId, inputs, outputs, onChangeNode } = data;
return (
<NodeCard minW={'400px'} moduleId={moduleId} {...props}>
<NodeCard minW={'400px'} {...data}>
<Container borderTop={'2px solid'} borderTopColor={'myGray.200'}>
<RenderInput moduleId={moduleId} onChangeNode={onChangeNode} flowInputList={inputs} />
</Container>

View File

@@ -13,11 +13,10 @@ import MyIcon from '@/components/Icon';
import { FlowOutputItemTypeEnum, FlowValueTypeEnum, SpecialInputKeyEnum } from '@/constants/flow';
import SourceHandle from '../render/SourceHandle';
const NodeCQNode = ({
data: { moduleId, inputs, outputs, onChangeNode, ...props }
}: NodeProps<FlowModuleItemType>) => {
const NodeCQNode = ({ data }: NodeProps<FlowModuleItemType>) => {
const { moduleId, inputs, outputs, onChangeNode } = data;
return (
<NodeCard minW={'400px'} moduleId={moduleId} {...props}>
<NodeCard minW={'400px'} {...data}>
<Divider text="Input" />
<Container>
<RenderInput

View File

@@ -13,16 +13,15 @@ import MySlider from '@/components/Slider';
import { Box } from '@chakra-ui/react';
import { formatPrice } from '@/utils/user';
const NodeChat = ({
data: { moduleId, inputs, outputs, onChangeNode, ...props }
}: NodeProps<FlowModuleItemType>) => {
const NodeChat = ({ data }: NodeProps<FlowModuleItemType>) => {
const { moduleId, inputs, outputs, onChangeNode } = data;
const outputsLen = useMemo(
() => outputs.filter((item) => item.type !== FlowOutputItemTypeEnum.hidden).length,
[outputs]
);
return (
<NodeCard minW={'400px'} moduleId={moduleId} {...props}>
<NodeCard minW={'400px'} {...data}>
<Divider text="Input" />
<Container>
<RenderInput

View File

@@ -3,7 +3,7 @@ import { NodeProps } from 'reactflow';
import NodeCard from '../modules/NodeCard';
import { FlowModuleItemType } from '@/types/flow';
const NodeAnswer = ({ data: { ...props } }: NodeProps<FlowModuleItemType>) => {
return <NodeCard {...props}></NodeCard>;
const NodeAnswer = ({ data }: NodeProps<FlowModuleItemType>) => {
return <NodeCard {...data}></NodeCard>;
};
export default React.memo(NodeAnswer);

View File

@@ -15,14 +15,13 @@ import ExtractFieldModal from '../modules/ExtractFieldModal';
import { ContextExtractEnum } from '@/constants/flow/flowField';
import { FlowOutputItemTypeEnum, FlowValueTypeEnum } from '@/constants/flow';
const NodeExtract = ({
data: { inputs, outputs, moduleId, onChangeNode, onDelEdge, ...props }
}: NodeProps<FlowModuleItemType>) => {
const NodeExtract = ({ data }: NodeProps<FlowModuleItemType>) => {
const { inputs, outputs, moduleId, onChangeNode, onDelEdge } = data;
const { t } = useTranslation();
const [editExtractFiled, setEditExtractField] = useState<ContextExtractAgentItemType>();
return (
<NodeCard minW={'400px'} moduleId={moduleId} {...props}>
<NodeCard minW={'400px'} {...data}>
<Divider text="Input" />
<Container>
<RenderInput

View File

@@ -7,22 +7,17 @@ import Container from '../modules/Container';
import RenderInput from '../render/RenderInput';
import RenderOutput from '../render/RenderOutput';
const NodeHistory = ({
data: { inputs, outputs, onChangeNode, ...props }
}: NodeProps<FlowModuleItemType>) => {
const NodeHistory = ({ data }: NodeProps<FlowModuleItemType>) => {
const { inputs, outputs, moduleId, onChangeNode } = data;
return (
<NodeCard minW={'300px'} {...props}>
<NodeCard minW={'300px'} {...data}>
<Divider text="Input" />
<Container>
<RenderInput moduleId={props.moduleId} onChangeNode={onChangeNode} flowInputList={inputs} />
<RenderInput moduleId={moduleId} onChangeNode={onChangeNode} flowInputList={inputs} />
</Container>
<Divider text="Output" />
<Container>
<RenderOutput
onChangeNode={onChangeNode}
moduleId={props.moduleId}
flowOutputList={outputs}
/>
<RenderOutput onChangeNode={onChangeNode} moduleId={moduleId} flowOutputList={outputs} />
</Container>
</NodeCard>
);

View File

@@ -13,11 +13,10 @@ import { FlowInputItemTypeEnum, FlowOutputItemTypeEnum, FlowValueTypeEnum } from
import { customAlphabet } from 'nanoid';
const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyz1234567890', 6);
const NodeHttp = ({
data: { moduleId, inputs, outputs, onChangeNode, ...props }
}: NodeProps<FlowModuleItemType>) => {
const NodeHttp = ({ data }: NodeProps<FlowModuleItemType>) => {
const { moduleId, inputs, outputs, onChangeNode } = data;
return (
<NodeCard minW={'350px'} moduleId={moduleId} {...props}>
<NodeCard minW={'350px'} {...data}>
<Container borderTop={'2px solid'} borderTopColor={'myGray.200'}>
<RenderInput moduleId={moduleId} onChangeNode={onChangeNode} flowInputList={inputs} />
<Button

View File

@@ -69,11 +69,10 @@ const KBSelect = ({
);
};
const NodeKbSearch = ({
data: { moduleId, inputs, outputs, onChangeNode, ...props }
}: NodeProps<FlowModuleItemType>) => {
const NodeKbSearch = ({ data }: NodeProps<FlowModuleItemType>) => {
const { moduleId, inputs, outputs, onChangeNode } = data;
return (
<NodeCard minW={'400px'} moduleId={moduleId} {...props}>
<NodeCard minW={'400px'} {...data}>
<Divider text="Input" />
<Container>
<RenderInput

View File

@@ -8,11 +8,9 @@ import { SystemInputEnum } from '@/constants/app';
import { FlowValueTypeEnum } from '@/constants/flow';
import SourceHandle from '../render/SourceHandle';
const QuestionInputNode = ({
data: { inputs, outputs, ...props }
}: NodeProps<FlowModuleItemType>) => {
const QuestionInputNode = ({ data }: NodeProps<FlowModuleItemType>) => {
return (
<NodeCard minW={'240px'} {...props}>
<NodeCard minW={'240px'} {...data}>
<Container borderTop={'2px solid'} borderTopColor={'myGray.200'} textAlign={'end'}>
<Box position={'relative'}>

View File

@@ -8,9 +8,9 @@ import Divider from '../modules/Divider';
import Container from '../modules/Container';
import Label from '../modules/Label';
const NodeTFSwitch = ({ data: { inputs, outputs, ...props } }: NodeProps<FlowModuleItemType>) => {
const NodeTFSwitch = ({ data }: NodeProps<FlowModuleItemType>) => {
return (
<NodeCard minW={'220px'} {...props}>
<NodeCard minW={'220px'} {...data}>
<Divider text="输入输出" />
<Container h={'100px'} py={0} px={0} display={'flex'} alignItems={'center'}>
<Box flex={1} pl={'12px'}>

View File

@@ -10,9 +10,8 @@ import MyIcon from '@/components/Icon';
import MyTooltip from '@/components/MyTooltip';
import { welcomeTextTip } from '@/constants/flow/ModuleTemplate';
const NodeUserGuide = ({
data: { inputs, outputs, onChangeNode, ...props }
}: NodeProps<FlowModuleItemType>) => {
const NodeUserGuide = ({ data }: NodeProps<FlowModuleItemType>) => {
const { inputs, moduleId, onChangeNode } = data;
const welcomeText = useMemo(
() => inputs.find((item) => item.key === SystemInputEnum.welcomeText),
[inputs]
@@ -20,7 +19,7 @@ const NodeUserGuide = ({
return (
<>
<NodeCard minW={'300px'} {...props}>
<NodeCard minW={'300px'} {...data}>
<Container borderTop={'2px solid'} borderTopColor={'myGray.200'}>
<>
<Flex mb={1} alignItems={'center'}>
@@ -40,7 +39,7 @@ const NodeUserGuide = ({
placeholder={welcomeTextTip}
onChange={(e) => {
onChangeNode({
moduleId: props.moduleId,
moduleId,
key: SystemInputEnum.welcomeText,
type: 'inputs',
value: {

View File

@@ -22,9 +22,8 @@ export const defaultVariable: VariableItemType = {
enums: [{ value: '' }]
};
const NodeUserGuide = ({
data: { inputs, outputs, onChangeNode, ...props }
}: NodeProps<FlowModuleItemType>) => {
const NodeUserGuide = ({ data }: NodeProps<FlowModuleItemType>) => {
const { inputs, moduleId, onChangeNode } = data;
const variables = useMemo(
() =>
(inputs.find((item) => item.key === SystemInputEnum.variables)
@@ -37,7 +36,7 @@ const NodeUserGuide = ({
const updateVariables = useCallback(
(value: VariableItemType[]) => {
onChangeNode({
moduleId: props.moduleId,
moduleId,
key: SystemInputEnum.variables,
type: 'inputs',
value: {
@@ -46,7 +45,7 @@ const NodeUserGuide = ({
}
});
},
[inputs, onChangeNode, props.moduleId]
[inputs, onChangeNode, moduleId]
);
const onclickSubmit = useCallback(
@@ -59,7 +58,7 @@ const NodeUserGuide = ({
return (
<>
<NodeCard minW={'300px'} {...props}>
<NodeCard minW={'300px'} {...data}>
<Container borderTop={'2px solid'} borderTopColor={'myGray.200'}>
<TableContainer>
<Table>

View File

@@ -6,29 +6,25 @@ import type { FlowModuleItemType } from '@/types/flow';
import MyTooltip from '@/components/MyTooltip';
import { QuestionOutlineIcon } from '@chakra-ui/icons';
import { useTranslation } from 'react-i18next';
import { useCopyData } from '@/utils/tools';
type Props = {
type Props = FlowModuleItemType & {
children?: React.ReactNode | React.ReactNode[] | string;
logo: string;
name: string;
description?: string;
intro: string;
minW?: string | number;
moduleId: string;
onDelNode: FlowModuleItemType['onDelNode'];
onCopyNode: FlowModuleItemType['onCopyNode'];
};
const NodeCard = ({
children,
logo = '/icon/logo.svg',
name = '未知模块',
description,
minW = '300px',
onCopyNode,
onDelNode,
moduleId
}: Props) => {
const NodeCard = (props: Props) => {
const {
children,
logo = '/icon/logo.svg',
name = '未知模块',
description,
minW = '300px',
onCopyNode,
onDelNode,
moduleId
} = props;
const { copyData } = useCopyData();
const { t } = useTranslation();
const theme = useTheme();
@@ -39,16 +35,22 @@ const NodeCard = ({
label: t('common.Copy'),
onClick: () => onCopyNode(moduleId)
},
// {
// icon: 'settingLight',
// label: t('app.Copy Module Config'),
// onClick: () => {
// const copyProps = { ...props };
// delete copyProps.children;
// delete copyProps.children;
// console.log(copyProps);
// }
// },
{
icon: 'delete',
label: t('common.Delete'),
onClick: () => onDelNode(moduleId)
},
// {
// icon: 'collectionLight',
// label: t('common.Collect'),
// onClick: () => {}
// },
{
icon: 'back',
label: t('common.Cancel'),

View File

@@ -1,7 +1,7 @@
import React, { useCallback, useRef } from 'react';
import Head from 'next/head';
import { useRouter } from 'next/router';
import { getInitChatSiteInfo, delChatRecordByIndex, putChatHistory } from '@/api/chat';
import { getInitChatSiteInfo, delChatRecordById, putChatHistory } from '@/api/chat';
import {
Box,
Flex,
@@ -128,7 +128,7 @@ const Chat = ({ appId, chatId }: { appId: string; chatId: string }) => {
...state,
history: state.history.filter((_, i) => i !== index)
}));
await delChatRecordByIndex({ chatId, contentId });
await delChatRecordById({ chatId, contentId });
} catch (err) {
console.log(err);
}

View File

@@ -147,10 +147,6 @@ const LoginForm = ({ setPageType, loginSuccess }: Props) => {
onClick={onclickGit}
/>
</Flex>
<Box mt={3} textAlign={'center'} fontSize={'sm'} color={'myGray.600'}>
Git Git 使 Git
</Box>
</>
)}
</form>