user timezone

This commit is contained in:
archer 2023-09-05 11:30:52 +08:00
parent 562fd2692d
commit 7a926b7086
No known key found for this signature in database
GPG Key ID: 569A5660D2379E28
18 changed files with 166 additions and 65 deletions

View File

@ -62,6 +62,7 @@
"remark-math": "^5.1.1", "remark-math": "^5.1.1",
"request-ip": "^3.3.0", "request-ip": "^3.3.0",
"sass": "^1.58.3", "sass": "^1.58.3",
"timezones-list": "^3.0.2",
"tunnel": "^0.0.6", "tunnel": "^0.0.6",
"winston": "^3.10.0", "winston": "^3.10.0",
"winston-mongodb": "^5.1.1", "winston-mongodb": "^5.1.1",

9
client/pnpm-lock.yaml generated
View File

@ -164,6 +164,9 @@ dependencies:
sass: sass:
specifier: ^1.58.3 specifier: ^1.58.3
version: registry.npmmirror.com/sass@1.58.3 version: registry.npmmirror.com/sass@1.58.3
timezones-list:
specifier: ^3.0.2
version: registry.npmmirror.com/timezones-list@3.0.2
tunnel: tunnel:
specifier: ^0.0.6 specifier: ^0.0.6
version: registry.npmmirror.com/tunnel@0.0.6 version: registry.npmmirror.com/tunnel@0.0.6
@ -11732,6 +11735,12 @@ packages:
version: 0.2.0 version: 0.2.0
dev: true dev: true
registry.npmmirror.com/timezones-list@3.0.2:
resolution: {integrity: sha512-I698hm6Jp/xxkwyTSOr39pZkYKETL8LDJeSIhjxXBfPUAHM5oZNuQ4o9UK3PSkDBOkjATecSOBb3pR1IkIBUsg==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/timezones-list/-/timezones-list-3.0.2.tgz}
name: timezones-list
version: 3.0.2
dev: false
registry.npmmirror.com/tiny-invariant@1.3.1: registry.npmmirror.com/tiny-invariant@1.3.1:
resolution: {integrity: sha512-AD5ih2NlSssTCwsMznbvwMZpJ1cbhkGd2uueNxzv2jDlEeZdU04JQfRnggJQ8DrcVBGjAsCKwFBbDlVNtEMlzw==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/tiny-invariant/-/tiny-invariant-1.3.1.tgz} resolution: {integrity: sha512-AD5ih2NlSssTCwsMznbvwMZpJ1cbhkGd2uueNxzv2jDlEeZdU04JQfRnggJQ8DrcVBGjAsCKwFBbDlVNtEMlzw==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/tiny-invariant/-/tiny-invariant-1.3.1.tgz}
name: tiny-invariant name: tiny-invariant

View File

@ -184,6 +184,7 @@
"Sign Out": "Sign Out", "Sign Out": "Sign Out",
"Source": "Source", "Source": "Source",
"Time": "Time", "Time": "Time",
"Timezone": "Timezone",
"Total Amount": "Total Amount", "Total Amount": "Total Amount",
"Update Password": "Update Password", "Update Password": "Update Password",
"Update password failed": "Update password failed", "Update password failed": "Update password failed",

View File

@ -184,6 +184,7 @@
"Sign Out": "登出", "Sign Out": "登出",
"Source": "来源", "Source": "来源",
"Time": "时间", "Time": "时间",
"Timezone": "时区",
"Total Amount": "总金额", "Total Amount": "总金额",
"Update Password": "修改密码", "Update Password": "修改密码",
"Update password failed": "修改密码异常", "Update password failed": "修改密码异常",

View File

@ -37,7 +37,7 @@ import { fileDownload } from '@/utils/file';
import { htmlTemplate } from '@/constants/common'; import { htmlTemplate } from '@/constants/common';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { useGlobalStore } from '@/store/global'; import { useGlobalStore } from '@/store/global';
import { TaskResponseKeyEnum, getDefaultChatVariables } from '@/constants/chat'; import { TaskResponseKeyEnum } from '@/constants/chat';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { customAlphabet } from 'nanoid'; import { customAlphabet } from 'nanoid';
import { userUpdateChatFeedback, adminUpdateChatFeedback } from '@/api/chat'; import { userUpdateChatFeedback, adminUpdateChatFeedback } from '@/api/chat';
@ -350,10 +350,7 @@ const ChatBox = (
messages, messages,
controller: abortSignal, controller: abortSignal,
generatingMessage, generatingMessage,
variables: { variables
...getDefaultChatVariables(),
...variables
}
}); });
// set finish status // set finish status

View File

@ -3,6 +3,7 @@ import { Menu, MenuButton, MenuItem, MenuList, MenuButtonProps } from '@chakra-u
import { getLangStore, LangEnum, setLangStore } from '@/utils/i18n'; import { getLangStore, LangEnum, setLangStore } from '@/utils/i18n';
import MyIcon from '@/components/Icon'; import MyIcon from '@/components/Icon';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useRouter } from 'next/router';
const langMap = { const langMap = {
[LangEnum.en]: { [LangEnum.en]: {
@ -16,6 +17,7 @@ const langMap = {
}; };
const Language = (props: MenuButtonProps) => { const Language = (props: MenuButtonProps) => {
const router = useRouter();
const { i18n } = useTranslation(); const { i18n } = useTranslation();
const [language, setLanguage] = useState<`${LangEnum}`>(getLangStore()); const [language, setLanguage] = useState<`${LangEnum}`>(getLangStore());
@ -43,6 +45,7 @@ const Language = (props: MenuButtonProps) => {
setLangStore(lang); setLangStore(lang);
setLanguage(lang); setLanguage(lang);
i18n?.changeLanguage?.(lang); i18n?.changeLanguage?.(lang);
router.reload();
}} }}
> >
{lang.label} {lang.label}

View File

@ -21,7 +21,7 @@ interface Props extends ButtonProps {
} }
const MySelect = ( const MySelect = (
{ placeholder, value, width = 'auto', list, onchange, ...props }: Props, { placeholder, value, width = '100%', list, onchange, ...props }: Props,
selectRef: any selectRef: any
) => { ) => {
const ref = useRef<HTMLButtonElement>(null); const ref = useRef<HTMLButtonElement>(null);
@ -94,6 +94,8 @@ const MySelect = (
} }
zIndex={99} zIndex={99}
transform={'translateY(35px) !important'} transform={'translateY(35px) !important'}
maxH={'40vh'}
overflowY={'auto'}
> >
{list.map((item) => ( {list.map((item) => (
<MenuItem <MenuItem

View File

@ -67,7 +67,3 @@ export enum OutLinkTypeEnum {
export const HUMAN_ICON = `/icon/human.png`; export const HUMAN_ICON = `/icon/human.png`;
export const LOGO_ICON = `/icon/logo.svg`; export const LOGO_ICON = `/icon/logo.svg`;
export const getDefaultChatVariables = () => ({
cTime: dayjs().format('YYYY/MM/DD HH:mm:ss')
});

View File

@ -1,5 +1,5 @@
import React, { useCallback } from 'react'; import React, { useCallback, useRef } from 'react';
import { Box, Flex, Button, useDisclosure, useTheme, Divider } from '@chakra-ui/react'; import { Box, Flex, Button, useDisclosure, useTheme, Divider, Select } from '@chakra-ui/react';
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
import { UserUpdateParams } from '@/types/user'; import { UserUpdateParams } from '@/types/user';
import { useToast } from '@/hooks/useToast'; import { useToast } from '@/hooks/useToast';
@ -11,6 +11,7 @@ import { useSelectFile } from '@/hooks/useSelectFile';
import { compressImg } from '@/utils/file'; import { compressImg } from '@/utils/file';
import { feConfigs } from '@/store/static'; import { feConfigs } from '@/store/static';
import { useTranslation } from 'next-i18next'; import { useTranslation } from 'next-i18next';
import { timezoneList } from '@/utils/user';
import Loading from '@/components/Loading'; import Loading from '@/components/Loading';
import Avatar from '@/components/Avatar'; import Avatar from '@/components/Avatar';
import MyIcon from '@/components/Icon'; import MyIcon from '@/components/Icon';
@ -33,6 +34,7 @@ const UserInfo = () => {
const theme = useTheme(); const theme = useTheme();
const { t } = useTranslation(); const { t } = useTranslation();
const { userInfo, updateUserInfo, initUserInfo } = useUserStore(); const { userInfo, updateUserInfo, initUserInfo } = useUserStore();
const timezones = useRef(timezoneList());
const { reset } = useForm<UserUpdateParams>({ const { reset } = useForm<UserUpdateParams>({
defaultValues: userInfo as UserType defaultValues: userInfo as UserType
}); });
@ -59,6 +61,7 @@ const UserInfo = () => {
async (data: UserType) => { async (data: UserType) => {
await updateUserInfo({ await updateUserInfo({
avatar: data.avatar, avatar: data.avatar,
timezone: data.timezone,
openaiAccount: data.openaiAccount openaiAccount: data.openaiAccount
}); });
reset(data); reset(data);
@ -102,7 +105,13 @@ const UserInfo = () => {
}); });
return ( return (
<Box display={['block', 'flex']} py={[2, 10]} justifyContent={'center'} fontSize={['lg', 'xl']}> <Box
display={['block', 'flex']}
py={[2, 10]}
justifyContent={'center'}
alignItems={'flex-start'}
fontSize={['lg', 'xl']}
>
<Flex <Flex
flexDirection={'column'} flexDirection={'column'}
alignItems={'center'} alignItems={'center'}
@ -135,11 +144,27 @@ const UserInfo = () => {
mt={[6, 0]} mt={[6, 0]}
> >
<Flex alignItems={'center'} w={['85%', '300px']}> <Flex alignItems={'center'} w={['85%', '300px']}>
<Box flex={'0 0 50px'}>{t('user.Account')}:&nbsp;</Box> <Box flex={'0 0 80px'}>{t('user.Account')}:&nbsp;</Box>
<Box flex={1}>{userInfo?.username}</Box> <Box flex={1}>{userInfo?.username}</Box>
</Flex> </Flex>
<Flex mt={6} alignItems={'center'} w={['85%', '300px']}> <Flex mt={6} alignItems={'center'} w={['85%', '300px']}>
<Box flex={'0 0 50px'}>{t('user.Password')}:&nbsp;</Box> <Box flex={'0 0 80px'}>{t('user.Timezone')}:&nbsp;</Box>
<Select
value={userInfo?.timezone}
onChange={(e) => {
if (!userInfo) return;
onclickSave({ ...userInfo, timezone: e.target.value });
}}
>
{timezones.current.map((item) => (
<option key={item.value} value={item.value}>
{item.name}
</option>
))}
</Select>
</Flex>
<Flex mt={6} alignItems={'center'} w={['85%', '300px']}>
<Box flex={'0 0 80px'}>{t('user.Password')}:&nbsp;</Box>
<Box flex={1}>*****</Box> <Box flex={1}>*****</Box>
<Button size={['sm', 'md']} variant={'base'} ml={5} onClick={onOpenUpdatePsw}> <Button size={['sm', 'md']} variant={'base'} ml={5} onClick={onOpenUpdatePsw}>
{t('user.Change')} {t('user.Change')}
@ -149,7 +174,7 @@ const UserInfo = () => {
<> <>
<Box mt={6} whiteSpace={'nowrap'} w={['85%', '300px']}> <Box mt={6} whiteSpace={'nowrap'} w={['85%', '300px']}>
<Flex alignItems={'center'}> <Flex alignItems={'center'}>
<Box flex={'0 0 50px'}>{t('user.Balance')}:&nbsp;</Box> <Box flex={'0 0 80px'}>{t('user.Balance')}:&nbsp;</Box>
<Box flex={1}> <Box flex={1}>
<strong>{userInfo?.balance.toFixed(3)}</strong> <strong>{userInfo?.balance.toFixed(3)}</strong>
</Box> </Box>

View File

@ -42,6 +42,10 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
/* user auth */ /* user auth */
const { userId, user } = await authUser({ req, authBalance: true }); const { userId, user } = await authUser({ req, authBalance: true });
if (!user) {
throw new Error('user not found');
}
/* start process */ /* start process */
const { responseData } = await dispatchModules({ const { responseData } = await dispatchModules({
res, res,

View File

@ -28,6 +28,7 @@ import { BillSourceEnum } from '@/constants/user';
import { ChatHistoryItemResType } from '@/types/chat'; import { ChatHistoryItemResType } from '@/types/chat';
import { UserModelSchema } from '@/types/mongoSchema'; import { UserModelSchema } from '@/types/mongoSchema';
import { SystemInputEnum } from '@/constants/app'; import { SystemInputEnum } from '@/constants/app';
import { getSystemTime } from '@/utils/user';
export type MessageItemType = ChatCompletionRequestMessage & { dataId?: string }; export type MessageItemType = ChatCompletionRequestMessage & { dataId?: string };
type FastGptWebChatProps = { type FastGptWebChatProps = {
@ -95,9 +96,6 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex
if (!user) { if (!user) {
throw new Error('Account is error'); throw new Error('Account is error');
} }
// if (authType === AuthUserTypeEnum.apikey || shareId) {
// user.openaiAccount = undefined;
// }
appId = appId ? appId : authAppid; appId = appId ? appId : authAppid;
if (!appId) { if (!appId) {
@ -249,6 +247,7 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex
} }
}); });
/* running */
export async function dispatchModules({ export async function dispatchModules({
res, res,
modules, modules,
@ -260,12 +259,16 @@ export async function dispatchModules({
}: { }: {
res: NextApiResponse; res: NextApiResponse;
modules: AppModuleItemType[]; modules: AppModuleItemType[];
user?: UserModelSchema; user: UserModelSchema;
params?: Record<string, any>; params?: Record<string, any>;
variables?: Record<string, any>; variables?: Record<string, any>;
stream?: boolean; stream?: boolean;
detail?: boolean; detail?: boolean;
}) { }) {
variables = {
...getSystemVariable({ timezone: user.timezone }),
...variables
};
const runningModules = loadModules(modules, variables); const runningModules = loadModules(modules, variables);
// let storeData: Record<string, any> = {}; // after module used // let storeData: Record<string, any> = {}; // after module used
@ -390,6 +393,7 @@ export async function dispatchModules({
}; };
} }
/* init store modules to running modules */
function loadModules( function loadModules(
modules: AppModuleItemType[], modules: AppModuleItemType[],
variables: Record<string, any> variables: Record<string, any>
@ -431,6 +435,7 @@ function loadModules(
}); });
} }
/* sse response modules staus */
export function responseStatus({ export function responseStatus({
res, res,
status, status,
@ -451,6 +456,13 @@ export function responseStatus({
}); });
} }
/* get system variable */
export function getSystemVariable({ timezone }: { timezone: string }) {
return {
cTime: getSystemTime(timezone)
};
}
export const config = { export const config = {
api: { api: {
bodyParser: { bodyParser: {

View File

@ -10,7 +10,7 @@ import { axiosConfig, getAIChatApi, openaiBaseUrl } from '@/service/lib/openai';
/* update user info */ /* update user info */
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) { export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
try { try {
const { avatar, openaiAccount } = req.body as UserUpdateParams; const { avatar, timezone, openaiAccount } = req.body as UserUpdateParams;
const { userId } = await authUser({ req, authToken: true }); const { userId } = await authUser({ req, authToken: true });
@ -46,6 +46,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
}, },
{ {
...(avatar && { avatar }), ...(avatar && { avatar }),
...(timezone && { timezone }),
openaiAccount: openaiAccount?.key ? openaiAccount : null openaiAccount: openaiAccount?.key ? openaiAccount : null
} }
); );

View File

@ -57,48 +57,7 @@ const AppSchema = new Schema({
default: [] default: []
}, },
// 弃 // 弃
chat: { chat: Object
relatedKbs: {
type: [Schema.Types.ObjectId],
ref: 'kb',
default: []
},
searchSimilarity: {
type: Number,
default: 0.4
},
searchLimit: {
type: Number,
default: 5
},
searchEmptyText: {
type: String,
default: ''
},
systemPrompt: {
type: String,
default: ''
},
limitPrompt: {
type: String,
default: ''
},
maxToken: {
type: Number,
default: 4000,
min: 100
},
temperature: {
type: Number,
min: 0,
max: 10,
default: 0
},
chatModel: {
// 聊天时使用的模型
type: String
}
}
}); });
try { try {

View File

@ -49,6 +49,10 @@ const UserSchema = new Schema({
key: String, key: String,
baseUrl: String baseUrl: String
} }
},
timezone: {
type: String,
default: 'Asia/Shanghai'
} }
}); });

View File

@ -1,7 +1,7 @@
import type { NextApiRequest } from 'next'; import type { NextApiRequest } from 'next';
import Cookie from 'cookie'; import Cookie from 'cookie';
import { App, OpenApi, User, OutLink, KB } from '../mongo'; import { App, OpenApi, User, OutLink, KB } from '../mongo';
import type { AppSchema } from '@/types/mongoSchema'; import type { AppSchema, UserModelSchema } from '@/types/mongoSchema';
import { ERROR_ENUM } from '../errorCode'; import { ERROR_ENUM } from '../errorCode';
import { authJWT } from './tools'; import { authJWT } from './tools';
@ -25,7 +25,10 @@ export const authCookieToken = async (cookie?: string, token?: string): Promise<
/* auth balance */ /* auth balance */
export const authBalanceByUid = async (uid: string) => { export const authBalanceByUid = async (uid: string) => {
const user = await User.findById(uid); const user = await User.findById<UserModelSchema>(
uid,
'_id username balance openaiAccount timezone'
);
if (!user) { if (!user) {
return Promise.reject(ERROR_ENUM.unAuthorization); return Promise.reject(ERROR_ENUM.unAuthorization);
} }

View File

@ -17,6 +17,7 @@ export interface UserModelSchema {
inviterId?: string; inviterId?: string;
openaiKey: string; openaiKey: string;
createTime: number; createTime: number;
timezone: string;
openaiAccount?: { openaiAccount?: {
key: string; key: string;
baseUrl: string; baseUrl: string;

View File

@ -5,6 +5,7 @@ export interface UserType {
username: string; username: string;
avatar: string; avatar: string;
balance: number; balance: number;
timezone: string;
promotionRate: UserModelSchema['promotionRate']; promotionRate: UserModelSchema['promotionRate'];
openaiAccount: UserModelSchema['openaiAccount']; openaiAccount: UserModelSchema['openaiAccount'];
} }
@ -12,6 +13,7 @@ export interface UserType {
export interface UserUpdateParams { export interface UserUpdateParams {
balance?: number; balance?: number;
avatar?: string; avatar?: string;
timezone?: string;
openaiAccount?: UserModelSchema['openaiAccount']; openaiAccount?: UserModelSchema['openaiAccount'];
} }

View File

@ -1,5 +1,12 @@
import { PRICE_SCALE } from '@/constants/common'; import { PRICE_SCALE } from '@/constants/common';
import { loginOut } from '@/api/user'; import { loginOut } from '@/api/user';
import timezones from 'timezones-list';
import dayjs from 'dayjs';
import utc from 'dayjs/plugin/utc';
import timezone from 'dayjs/plugin/timezone';
dayjs.extend(utc);
dayjs.extend(timezone);
const tokenKey = 'token'; const tokenKey = 'token';
export const clearToken = () => { export const clearToken = () => {
@ -24,3 +31,76 @@ export const getToken = () => {
export const formatPrice = (val = 0, multiple = 1) => { export const formatPrice = (val = 0, multiple = 1) => {
return Number(((val / PRICE_SCALE) * multiple).toFixed(10)); return Number(((val / PRICE_SCALE) * multiple).toFixed(10));
}; };
/**
* Returns the offset from UTC in hours for the current locale.
* @param {string} timeZone Timezone to get offset for
* @returns {number} The offset from UTC in hours.
*
* Generated by Trelent
*/
export const getTimezoneOffset = (timeZone: string): number => {
const now = new Date();
const tzString = now.toLocaleString('en-US', {
timeZone
});
const localString = now.toLocaleString('en-US');
const diff = (Date.parse(localString) - Date.parse(tzString)) / 3600000;
const offset = diff + now.getTimezoneOffset() / 60;
return -offset;
};
/**
* Returns a list of timezones sorted by their offset from UTC.
* @returns {object[]} A list of the given timezones sorted by their offset from UTC.
*
* Generated by Trelent
*/
export const timezoneList = () => {
const result = timezones
.map((timezone) => {
try {
let display = dayjs().tz(timezone.tzCode).format('Z');
return {
name: `(UTC${display}) ${timezone.tzCode}`,
value: timezone.tzCode,
time: getTimezoneOffset(timezone.tzCode)
};
} catch (e) {}
})
.filter((item) => item);
result.sort((a, b) => {
if (!a || !b) return 0;
if (a.time > b.time) {
return 1;
}
if (b.time > a.time) {
return -1;
}
return 0;
});
return [
{
name: 'UTC',
time: 0,
value: 'UTC'
},
...result
] as {
name: string;
value: string;
time: number;
}[];
};
export const getSystemTime = (timeZone: string) => {
const timezoneDiff = getTimezoneOffset(timeZone);
const now = Date.now();
const targetTime = now + timezoneDiff * 60 * 60 * 1000;
return dayjs(targetTime).format('YYYY-MM-DD HH:mm:ss');
};