System inform (#2263)

* feat: Bind Notification Pipe (#2229)

* chore: account page add bind notification modal

* feat: timerlock schema and type

* feat(fe): bind notification method modal

* chore: fe adjust

* feat: clean useless code

* fix: cron lock

* chore: adjust the code

* chore: rename api

* chore: remove unused code

* chore: fe adjust

* perf: bind inform ux

* fix: time ts

* chore: notification (#2251)

* perf: send message code

* perf: sub schema index

* fix: timezone plugin

* fix: format

---------

Co-authored-by: Finley Ge <32237950+FinleyGe@users.noreply.github.com>
This commit is contained in:
Archer
2024-08-05 00:29:14 +08:00
committed by GitHub
parent 998e7833e8
commit 56f6e69bc7
31 changed files with 344 additions and 171 deletions

View File

@@ -50,6 +50,7 @@ const StandDetailModal = dynamic(() => import('./standardDetailModal'));
const TeamMenu = dynamic(() => import('@/components/support/user/team/TeamMenu'));
const PayModal = dynamic(() => import('./PayModal'));
const UpdatePswModal = dynamic(() => import('./UpdatePswModal'));
const UpdateNotification = dynamic(() => import('./UpdateNotificationModal'));
const OpenAIAccountModal = dynamic(() => import('./OpenAIAccountModal'));
const LafAccountModal = dynamic(() => import('@/components/support/laf/LafAccountModal'));
const CommunityModal = dynamic(() => import('@/components/CommunityModal'));
@@ -113,6 +114,11 @@ const MyInfo = () => {
onClose: onCloseUpdatePsw,
onOpen: onOpenUpdatePsw
} = useDisclosure();
const {
isOpen: isOpenUpdateNotification,
onClose: onCloseUpdateNotification,
onOpen: onOpenUpdateNotification
} = useDisclosure();
const { File, onOpen: onOpenSelectFile } = useSelectFile({
fileType: '.jpg,.png',
multiple: false
@@ -225,7 +231,7 @@ const MyInfo = () => {
</Flex>
</Flex>
)}
{feConfigs.isPlus && (
{feConfigs?.isPlus && (
<Flex mt={[0, 4]} alignItems={'center'}>
<Box {...labelStyles}>{t('common:user.Member Name')}:&nbsp;</Box>
<Input
@@ -249,7 +255,7 @@ const MyInfo = () => {
<Box {...labelStyles}>{t('common:user.Account')}:&nbsp;</Box>
<Box flex={1}>{userInfo?.username}</Box>
</Flex>
{feConfigs.isPlus && (
{feConfigs?.isPlus && (
<Flex mt={6} alignItems={'center'}>
<Box {...labelStyles}>{t('common:user.Password')}:&nbsp;</Box>
<Box flex={1}>*****</Box>
@@ -258,13 +264,27 @@ const MyInfo = () => {
</Button>
</Flex>
)}
{feConfigs?.isPlus && (
<Flex mt={6} alignItems={'center'}>
<Box {...labelStyles}>{t('common:user.Notification Receive')}:&nbsp;</Box>
<Box flex={1} {...(userInfo?.team.notificationAccount ? {} : { color: 'red.600' })}>
{userInfo?.team.notificationAccount || t('common:user.Notification Receive Bind')}
</Box>
{userInfo?.permission.isOwner && (
<Button size={'sm'} variant={'whitePrimary'} onClick={onOpenUpdateNotification}>
{t('common:user.Change')}
</Button>
)}
</Flex>
)}
<Flex mt={6} alignItems={'center'}>
<Box {...labelStyles}>{t('common:user.Team')}:&nbsp;</Box>
<Box flex={1}>
<TeamMenu />
</Box>
</Flex>
{feConfigs.isPlus && (
{feConfigs?.isPlus && (
<Box mt={6} whiteSpace={'nowrap'}>
<Flex alignItems={'center'}>
<Box {...labelStyles}>{t('common:user.team.Balance')}:&nbsp;</Box>
@@ -282,6 +302,7 @@ const MyInfo = () => {
</Box>
{isOpenPayModal && <PayModal onClose={onClosePayModal} />}
{isOpenUpdatePsw && <UpdatePswModal onClose={onCloseUpdatePsw} />}
{isOpenUpdateNotification && <UpdateNotification onClose={onCloseUpdateNotification} />}
<File onSelect={onSelectFile} />
</Box>
);
@@ -579,7 +600,7 @@ const Other = () => {
)}
{feConfigs?.chatbotUrl && (
<Link
href={feConfigs.chatbotUrl}
href={feConfigs?.chatbotUrl}
target="_blank"
display={'flex'}
py={3}

View File

@@ -0,0 +1,120 @@
import React, { useCallback } from 'react';
import { ModalBody, Box, Flex, Input, ModalFooter, Button, HStack } from '@chakra-ui/react';
import MyModal from '@fastgpt/web/components/common/MyModal';
import { useTranslation } from 'next-i18next';
import { useForm } from 'react-hook-form';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { updateNotificationAccount } from '@/web/support/user/api';
import Icon from '@fastgpt/web/components/common/Icon';
import { useSendCode } from '@/web/support/user/hooks/useSendCode';
import { useUserStore } from '@/web/support/user/useUserStore';
type FormType = {
account: string;
verifyCode: string;
};
const UpdateNotificationModal = ({ onClose }: { onClose: () => void }) => {
const { t } = useTranslation();
const { initUserInfo } = useUserStore();
const { register, handleSubmit, trigger, getValues, watch } = useForm<FormType>({
defaultValues: {
account: '',
verifyCode: ''
}
});
const account = watch('account');
const verifyCode = watch('verifyCode');
const { runAsync: onSubmit, loading: isLoading } = useRequest2(
(data: FormType) => {
return updateNotificationAccount(data);
},
{
onSuccess() {
initUserInfo();
onClose();
},
successToast: t('user:bind_inform_account_success'),
errorToast: t('user:bind_inform_account_error')
}
);
const { sendCodeText, sendCode, codeCountDown } = useSendCode();
const onclickSendCode = useCallback(async () => {
const check = await trigger('account');
if (!check) return;
sendCode({
username: getValues('account'),
type: 'bindNotification'
});
}, [getValues, sendCode, trigger]);
return (
<MyModal
isOpen
iconSrc="common/settingLight"
w={'32rem'}
title={t('common:user.Notification Receive')}
>
<ModalBody px={10}>
<Flex flexDirection="column">
<HStack px="6" py="3" color="primary.600" bgColor="primary.50" borderRadius="md">
<Icon name="common/info" w="1rem" />
<Box fontSize={'sm'}>{t('user:notification.Bind Notification Pipe Hint')}</Box>
</HStack>
<Flex mt="4" alignItems="center">
<Box flex={'0 0 70px'}>{t('common:user.Account')}</Box>
<Input
flex={1}
bg={'myGray.50'}
{...register('account', { required: true })}
placeholder={t('common:support.user.Email Or Phone')}
></Input>
</Flex>
<Flex mt="6" alignItems="center" position={'relative'}>
<Box flex={'0 0 70px'}>{t('common:support.user.Verify Code')}</Box>
<Input
flex={1}
bg={'myGray.50'}
{...register('verifyCode', { required: true })}
placeholder={t('common:support.user.Verify Code')}
></Input>
<Box
position={'absolute'}
right={2}
zIndex={1}
fontSize={'sm'}
{...(codeCountDown > 0
? {
color: 'myGray.500'
}
: {
color: 'primary.700',
cursor: 'pointer',
onClick: onclickSendCode
})}
>
{sendCodeText}
</Box>
</Flex>
</Flex>
</ModalBody>
<ModalFooter>
<Button mr={3} variant={'whiteBase'} onClick={onClose}>
{t('common:common.Cancel')}
</Button>
<Button
isLoading={isLoading}
isDisabled={!account || !verifyCode}
onClick={handleSubmit((data) => onSubmit(data))}
>
{t('common:common.Confirm')}
</Button>
</ModalFooter>
</MyModal>
);
};
export default UpdateNotificationModal;

View File

@@ -6,6 +6,7 @@ import { connectToDatabase } from '@/service/mongo';
import { getUserDetail } from '@fastgpt/service/support/user/controller';
import type { PostLoginProps } from '@fastgpt/global/support/user/api.d';
import { UserStatusEnum } from '@fastgpt/global/support/user/constant';
import { checkTeamAiPointsAndLock } from '@/service/events/utils';
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try {

View File

@@ -1,21 +1,15 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@fastgpt/service/common/response';
import { authCert } from '@fastgpt/service/support/permission/auth/common';
import { connectToDatabase } from '@/service/mongo';
import { getUserDetail } from '@fastgpt/service/support/user/controller';
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try {
await connectToDatabase();
const { tmbId } = await authCert({ req, authToken: true });
jsonRes(res, {
data: await getUserDetail({ tmbId })
});
} catch (err) {
jsonRes(res, {
code: 500,
error: err
});
}
import type { ApiRequestProps, ApiResponseType } from '@fastgpt/service/type/next';
import { NextAPI } from '@/service/middleware/entry';
export type TokenLoginQuery = {};
export type TokenLoginBody = {};
export type TokenLoginResponse = {};
async function handler(
req: ApiRequestProps<TokenLoginBody, TokenLoginQuery>,
_res: ApiResponseType<any>
): Promise<TokenLoginResponse> {
const { tmbId } = await authCert({ req, authToken: true });
return getUserDetail({ tmbId });
}
export default NextAPI(handler);

View File

@@ -1,62 +1,60 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@fastgpt/service/common/response';
import { MongoUser } from '@fastgpt/service/support/user/schema';
import { authCert } from '@fastgpt/service/support/permission/auth/common';
import { UserUpdateParams } from '@/types/user';
import { getAIApi, openaiBaseUrl } from '@fastgpt/service/core/ai/config';
import { connectToDatabase } from '@/service/mongo';
import { MongoTeamMember } from '@fastgpt/service/support/user/team/teamMemberSchema';
/* update user info */
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
try {
await connectToDatabase();
const { avatar, timezone, openaiAccount, lafAccount } = req.body as UserUpdateParams;
import type { ApiRequestProps, ApiResponseType } from '@fastgpt/service/type/next';
import { NextAPI } from '@/service/middleware/entry';
export type UserAccountUpdateQuery = {};
export type UserAccountUpdateBody = UserUpdateParams;
export type UserAccountUpdateResponse = {};
async function handler(
req: ApiRequestProps<UserAccountUpdateBody, UserAccountUpdateQuery>,
_res: ApiResponseType<any>
): Promise<UserAccountUpdateResponse> {
const { avatar, timezone, openaiAccount, lafAccount } = req.body;
const { tmbId } = await authCert({ req, authToken: true });
const tmb = await MongoTeamMember.findById(tmbId);
if (!tmb) {
throw new Error('can not find it');
}
const userId = tmb.userId;
// auth key
if (openaiAccount?.key) {
console.log('auth user openai key', openaiAccount?.key);
const baseUrl = openaiAccount?.baseUrl || openaiBaseUrl;
openaiAccount.baseUrl = baseUrl;
const ai = getAIApi({
userKey: openaiAccount
});
const response = await ai.chat.completions.create({
model: 'gpt-4o-mini',
max_tokens: 1,
messages: [{ role: 'user', content: 'hi' }]
});
if (response?.choices?.[0]?.message?.content === undefined) {
throw new Error('Key response is empty');
}
}
// 更新对应的记录
await MongoUser.updateOne(
{
_id: userId
},
{
...(avatar && { avatar }),
...(timezone && { timezone }),
openaiAccount: openaiAccount?.key ? openaiAccount : null,
lafAccount: lafAccount?.token ? lafAccount : null
}
);
jsonRes(res);
} catch (err) {
jsonRes(res, {
code: 500,
error: err
});
const { tmbId } = await authCert({ req, authToken: true });
const tmb = await MongoTeamMember.findById(tmbId);
if (!tmb) {
throw new Error('can not find it');
}
const userId = tmb.userId;
// auth key
if (openaiAccount?.key) {
console.log('auth user openai key', openaiAccount?.key);
const baseUrl = openaiAccount?.baseUrl || openaiBaseUrl;
openaiAccount.baseUrl = baseUrl;
const ai = getAIApi({
userKey: openaiAccount
});
const response = await ai.chat.completions.create({
model: 'gpt-4o-mini',
max_tokens: 1,
messages: [{ role: 'user', content: 'hi' }]
});
if (response?.choices?.[0]?.message?.content === undefined) {
throw new Error('Key response is empty');
}
}
// 更新对应的记录
await MongoUser.updateOne(
{
_id: userId
},
{
...(avatar && { avatar }),
...(timezone && { timezone }),
openaiAccount: openaiAccount?.key ? openaiAccount : null,
lafAccount: lafAccount?.token ? lafAccount : null
}
);
return {};
}
export default NextAPI(handler);

View File

@@ -49,7 +49,6 @@ const ListItem = () => {
const { parentId = null } = router.query;
const { isPc } = useSystem();
const { lastChatAppId, setLastChatAppId } = useChatStore();
const { myApps, loadMyApps, onUpdateApp, setMoveAppId, folderDetail } = useContextSelector(
AppListContext,

View File

@@ -88,7 +88,7 @@ export async function generateQA(): Promise<any> {
}
// auth balance
if (!(await checkTeamAiPointsAndLock(data.teamId, data.tmbId))) {
if (!(await checkTeamAiPointsAndLock(data.teamId))) {
reduceQueue();
return generateQA();
}

View File

@@ -89,7 +89,7 @@ export async function generateVector(): Promise<any> {
}
// auth balance
if (!(await checkTeamAiPointsAndLock(data.teamId, data.tmbId))) {
if (!(await checkTeamAiPointsAndLock(data.teamId))) {
reduceQueue();
return generateVector();
}

View File

@@ -4,7 +4,7 @@ import { sendOneInform } from '../support/user/inform/api';
import { lockTrainingDataByTeamId } from '@fastgpt/service/core/dataset/training/controller';
import { InformLevelEnum } from '@fastgpt/global/support/user/inform/constants';
export const checkTeamAiPointsAndLock = async (teamId: string, tmbId: string) => {
export const checkTeamAiPointsAndLock = async (teamId: string) => {
try {
await checkTeamAIPoints(teamId);
return true;
@@ -13,11 +13,10 @@ export const checkTeamAiPointsAndLock = async (teamId: string, tmbId: string) =>
// send inform and lock data
try {
sendOneInform({
level: InformLevelEnum.important,
title: '文本训练任务中止',
content:
'该团队账号AI积分不足文本训练任务中止重新充值后将会继续。暂停的任务将在 7 天后被删除。',
tmbId: tmbId
level: InformLevelEnum.emergency,
templateCode: 'LACK_OF_POINTS',
templateParam: {},
teamId
});
console.log('余额不足,暂停【向量】生成任务');
lockTrainingDataByTeamId(teamId);

View File

@@ -63,6 +63,9 @@ export const updatePasswordByOld = ({ oldPsw, newPsw }: { oldPsw: string; newPsw
newPsw: hashStr(newPsw)
});
export const updateNotificationAccount = (data: { account: string; verifyCode: string }) =>
PUT('/proApi/support/user/team/updateNotificationAccount', data);
export const postLogin = ({ password, ...props }: PostLoginProps) =>
POST<ResLogin>('/support/user/account/loginByPassword', {
...props,

View File

@@ -1,19 +1,40 @@
import { useState, useMemo, useCallback } from 'react';
import { useState, useMemo } from 'react';
import { sendAuthCode } from '@/web/support/user/api';
import { UserAuthTypeEnum } from '@fastgpt/global/support/user/auth/constants';
import { useToast } from '@fastgpt/web/hooks/useToast';
import { getErrText } from '@fastgpt/global/common/error/utils';
import { useTranslation } from 'next-i18next';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
let timer: any;
let timer: NodeJS.Timeout;
export const useSendCode = () => {
const { t } = useTranslation();
const { toast } = useToast();
const { feConfigs } = useSystemStore();
const [codeSending, setCodeSending] = useState(false);
const [codeCountDown, setCodeCountDown] = useState(0);
const { runAsync: sendCode, loading: codeSending } = useRequest2(
async ({ username, type }: { username: string; type: `${UserAuthTypeEnum}` }) => {
if (codeCountDown > 0) return;
const googleToken = await getClientToken(feConfigs.googleClientVerKey);
await sendAuthCode({ username, type, googleToken });
setCodeCountDown(60);
timer = setInterval(() => {
setCodeCountDown((val) => {
if (val <= 0) {
clearInterval(timer);
}
return val - 1;
});
}, 1000);
},
{
successToast: '验证码已发送',
errorToast: '验证码发送异常',
refreshDeps: [codeCountDown, feConfigs?.googleClientVerKey]
}
);
const sendCodeText = useMemo(() => {
if (codeSending) return t('common:support.user.auth.Sending Code');
if (codeCountDown >= 10) {
@@ -25,41 +46,6 @@ export const useSendCode = () => {
return '获取验证码';
}, [codeCountDown, codeSending, t]);
const sendCode = useCallback(
async ({ username, type }: { username: string; type: `${UserAuthTypeEnum}` }) => {
if (codeCountDown > 0) return;
setCodeSending(true);
try {
await sendAuthCode({
username,
type,
googleToken: await getClientToken(feConfigs.googleClientVerKey)
});
setCodeCountDown(60);
timer = setInterval(() => {
setCodeCountDown((val) => {
if (val <= 0) {
clearInterval(timer);
}
return val - 1;
});
}, 1000);
toast({
title: '验证码已发送',
status: 'success',
position: 'top'
});
} catch (error: any) {
toast({
title: getErrText(error, '验证码发送异常'),
status: 'error'
});
}
setCodeSending(false);
},
[codeCountDown, feConfigs?.googleClientVerKey, toast]
);
return {
codeSending,
sendCode,