Commit 3d7178d0 by Archer Committed by GitHub

monorepo packages (#344)

parent a4ff5a3f
......@@ -3,7 +3,7 @@ on:
workflow_dispatch:
push:
paths:
- 'client/**'
- 'projects/app/**'
branches:
- 'main'
tags:
......@@ -49,8 +49,8 @@ jobs:
env:
DOCKER_REPO_TAGGED: ${{ env.DOCKER_REPO_TAGGED }}
run: |
cd client && \
docker buildx build \
--build-arg name=app \
--platform linux/amd64,linux/arm64 \
--label "org.opencontainers.image.source= https://github.com/ ${{ github.repository_owner }}/FastGPT" \
--label "org.opencontainers.image.description=fastgpt image" \
......@@ -64,6 +64,7 @@ jobs:
push-to-docker-hub:
needs: build-fastgpt-images
runs-on: ubuntu-20.04
if: github.repository == 'labring/FastGPT'
steps:
- name: Checkout code
uses: actions/checkout@v3
......@@ -87,6 +88,7 @@ jobs:
run: docker push ${{ secrets.DOCKER_IMAGE_NAME }}:${{env.IMAGE_TAG}}
push-to-ali-hub:
needs: build-fastgpt-images
if: github.repository == 'labring/FastGPT'
runs-on: ubuntu-20.04
steps:
- name: Checkout code
......
{
"editor.formatOnSave": true,
"editor.mouseWheelZoom": true,
"typescript.tsdk": "client/node_modules/typescript/lib",
"typescript.tsdk": "node_modules/typescript/lib",
"prettier.prettierPath": "./node_modules/prettier",
"i18n-ally.localesPaths": [
"client/public/locales"
"projects/app/public/locales"
],
"i18n-ally.enabledParsers": ["json"],
"i18n-ally.keystyle": "nested",
......
......@@ -4,39 +4,44 @@ FROM node:current-alpine AS deps
RUN apk add --no-cache libc6-compat && npm install -g pnpm
WORKDIR /app
# Install dependencies based on the preferred package manager
COPY package.json ./
COPY pnpm-lock.yaml* ./
ARG name
# copy packages and one project
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
COPY ./packages ./packages
COPY ./projects/$name/package.json ./projects/$name/package.json
COPY ./projects/$name/pnpm-lock.yaml ./projects/$name/pnpm-lock.yaml
RUN \
[ -f pnpm-lock.yaml ] && pnpm fetch || \
[ -f pnpm-lock.yaml ] && pnpm install || \
(echo "Lockfile not found." && exit 1)
RUN pnpm prune
# Rebuild the source code only when needed
FROM node:current-alpine AS builder
WORKDIR /app
ARG name
# copy common node_modules and one project node_modules
COPY --from=deps /app/node_modules ./node_modules
COPY pnpm-lock.yaml* ./
COPY package.json ./
COPY . .
COPY ./projects/$name ./projects/$name
COPY --from=deps /app/projects/$name/node_modules ./projects/$name/node_modules
COPY pnpm-lock.yaml pnpm-workspace.yaml ./
COPY ./packages ./packages
# Next.js collects completely anonymous telemetry data about general usage.
# Learn more here: https://nextjs.org/telemetry
# Uncomment the following line in case you want to disable telemetry during the build.
ENV NEXT_TELEMETRY_DISABLED 1
RUN npm install -g pnpm
RUN \
[ -f pnpm-lock.yaml ] && (pnpm --offline install && pnpm run build) || \
(echo "Lockfile not found." && exit 1)
RUN pnpm --filter=$name run build
# Production image, copy all the files and run next
FROM node:current-alpine AS runner
WORKDIR /app
ENV NODE_ENV production
# Uncomment the following line in case you want to disable telemetry during runtime.
ENV NEXT_TELEMETRY_DISABLED 1
ARG name
# create user and use it
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
......@@ -45,21 +50,22 @@ RUN apk add curl \
&& apk add ca-certificates \
&& update-ca-certificates
# You only need to copy next.config.js if you are NOT using the default configuration
# COPY --from=builder /app/next.config.js ./
COPY --from=builder /app/public ./public
COPY --from=builder /app/package.json ./package.json
# COPY --from=builder /app/.env* .
# Automatically leverage output traces to reduce image size
# https://nextjs.org/docs/advanced-features/output-file-tracing
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
USER nextjs
# copy running files
COPY --from=builder /app/projects/$name/public ./projects/$name/public
COPY --from=builder /app/projects/$name/next.config.js ./projects/$name/next.config.js
COPY --from=builder --chown=nextjs:nodejs /app/projects/$name/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/projects/$name/.next/static ./projects/$name/.next/static
# copy package.json to version file
COPY --from=builder /app/projects/$name/package.json ./package.json
ENV NODE_ENV production
ENV NEXT_TELEMETRY_DISABLED 1
ENV PORT=3000
EXPOSE 3000
CMD ["node", "server.js"]
USER nextjs
ENV serverPath=./projects/$name/server.js
ENTRYPOINT ["sh","-c","node ${serverPath}"]
\ No newline at end of file
/// <reference types="next" />
/// <reference types="next/image-types/global" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/basic-features/typescript for more information.
import dayjs from 'dayjs';
export enum sseResponseEventEnum {
error = 'error',
answer = 'answer',
moduleStatus = 'moduleStatus',
appStreamResponse = 'appStreamResponse' // sse response request
}
export enum ChatRoleEnum {
System = 'System',
Human = 'Human',
AI = 'AI'
}
export enum TaskResponseKeyEnum {
'answerText' = 'answerText', // answer module text key
'responseData' = 'responseData'
}
export const ChatRoleMap = {
[ChatRoleEnum.System]: {
name: '系统提示词'
},
[ChatRoleEnum.Human]: {
name: '用户'
},
[ChatRoleEnum.AI]: {
name: 'AI'
}
};
export enum ChatSourceEnum {
test = 'test',
online = 'online',
share = 'share',
api = 'api'
}
export const ChatSourceMap = {
[ChatSourceEnum.test]: {
name: 'chat.logs.test'
},
[ChatSourceEnum.online]: {
name: 'chat.logs.online'
},
[ChatSourceEnum.share]: {
name: 'chat.logs.share'
},
[ChatSourceEnum.api]: {
name: 'chat.logs.api'
}
};
export enum OutLinkTypeEnum {
'share' = 'share',
'iframe' = 'iframe',
apikey = 'apikey'
}
export const HUMAN_ICON = `/icon/human.png`;
export const LOGO_ICON = `/icon/logo.svg`;
import type { BoxProps } from '@chakra-ui/react';
export enum FlowInputItemTypeEnum {
systemInput = 'systemInput', // history, userChatInput, variableInput
input = 'input',
textarea = 'textarea',
numberInput = 'numberInput',
select = 'select',
slider = 'slider',
custom = 'custom',
target = 'target',
none = 'none',
hidden = 'hidden'
}
export enum FlowOutputItemTypeEnum {
answer = 'answer',
source = 'source',
none = 'none',
hidden = 'hidden'
}
export enum FlowModuleTypeEnum {
empty = 'empty',
variable = 'variable',
userGuide = 'userGuide',
questionInput = 'questionInput',
historyNode = 'historyNode',
chatNode = 'chatNode',
kbSearchNode = 'kbSearchNode',
tfSwitchNode = 'tfSwitchNode',
answerNode = 'answerNode',
classifyQuestion = 'classifyQuestion',
contentExtract = 'contentExtract',
httpRequest = 'httpRequest'
}
export enum SpecialInputKeyEnum {
'answerText' = 'text',
'agents' = 'agents' // cq agent key
}
export enum FlowValueTypeEnum {
'string' = 'string',
'number' = 'number',
'boolean' = 'boolean',
'chatHistory' = 'chat_history',
'kbQuote' = 'kb_quote',
'any' = 'any'
}
export const FlowValueTypeStyle: Record<`${FlowValueTypeEnum}`, BoxProps> = {
[FlowValueTypeEnum.string]: {
background: '#36ADEF'
},
[FlowValueTypeEnum.number]: {
background: '#FB7C3C'
},
[FlowValueTypeEnum.boolean]: {
background: '#E7D118'
},
[FlowValueTypeEnum.chatHistory]: {
background: '#00A9A6'
},
[FlowValueTypeEnum.kbQuote]: {
background: '#A558C9'
},
[FlowValueTypeEnum.any]: {
background: '#9CA2A8'
}
};
export const initModuleType: Record<string, boolean> = {
[FlowModuleTypeEnum.historyNode]: true,
[FlowModuleTypeEnum.questionInput]: true
};
export const edgeOptions = {
style: {
strokeWidth: 1.5,
stroke: '#5A646Es'
}
};
export const connectionLineStyle = { strokeWidth: 1.5, stroke: '#5A646Es' };
export enum OAuthEnum {
github = 'github',
google = 'google'
}
export enum BillSourceEnum {
fastgpt = 'fastgpt',
api = 'api',
shareLink = 'shareLink',
training = 'training'
}
export enum PageTypeEnum {
login = 'login',
register = 'register',
forgetPassword = 'forgetPassword'
}
export const BillSourceMap: Record<`${BillSourceEnum}`, string> = {
[BillSourceEnum.fastgpt]: '在线使用',
[BillSourceEnum.api]: 'Api',
[BillSourceEnum.shareLink]: '免登录链接',
[BillSourceEnum.training]: '数据训练'
};
export enum PromotionEnum {
register = 'register',
pay = 'pay'
}
export enum InformTypeEnum {
system = 'system'
}
export const InformTypeMap = {
[InformTypeEnum.system]: {
label: '系统通知'
}
};
export enum MyModelsTypeEnum {
my = 'my',
collection = 'collection'
}
import { useCallback, useRef, useState } from 'react';
import {
AlertDialog,
AlertDialogBody,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogContent,
AlertDialogOverlay,
useDisclosure,
Button
} from '@chakra-ui/react';
import { useTranslation } from 'next-i18next';
export const useConfirm = (props: { title?: string | null; content?: string | null }) => {
const { t } = useTranslation();
const { title = t('Warning'), content } = props;
const [customContent, setCustomContent] = useState(content);
const { isOpen, onOpen, onClose } = useDisclosure();
const cancelRef = useRef(null);
const confirmCb = useRef<any>();
const cancelCb = useRef<any>();
return {
openConfirm: useCallback(
(confirm?: any, cancel?: any, customContent?: string) => {
confirmCb.current = confirm;
cancelCb.current = cancel;
customContent && setCustomContent(customContent);
return onOpen;
},
[onOpen]
),
ConfirmModal: useCallback(
() => (
<AlertDialog
isOpen={isOpen}
leastDestructiveRef={cancelRef}
autoFocus={false}
onClose={onClose}
>
<AlertDialogOverlay>
<AlertDialogContent maxW={'min(90vw,400px)'}>
<AlertDialogHeader fontSize="lg" fontWeight="bold">
{title}
</AlertDialogHeader>
<AlertDialogBody>{customContent}</AlertDialogBody>
<AlertDialogFooter>
<Button
variant={'base'}
onClick={() => {
onClose();
typeof cancelCb.current === 'function' && cancelCb.current();
}}
>
{t('Cancel')}
</Button>
<Button
ml={4}
onClick={() => {
onClose();
typeof confirmCb.current === 'function' && confirmCb.current();
}}
>
{t('Confirm')}
</Button>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialogOverlay>
</AlertDialog>
),
[customContent, isOpen, onClose, t, title]
)
};
};
import { useTranslation } from 'react-i18next';
import { useToast } from './useToast';
/**
* copy text data
*/
export const useCopyData = () => {
const { t } = useTranslation();
const { toast } = useToast();
return {
copyData: async (
data: string,
title: string | null = t('common.Copy Successful'),
duration = 1000
) => {
try {
if (navigator.clipboard) {
await navigator.clipboard.writeText(data);
} else {
throw new Error('');
}
} catch (error) {
const textarea = document.createElement('textarea');
textarea.value = data;
document.body.appendChild(textarea);
textarea.select();
document.execCommand('copy');
document.body.removeChild(textarea);
}
toast({
title,
status: 'success',
duration
});
}
};
};
import React, { useCallback, useRef } from 'react';
import { ModalFooter, ModalBody, Input, useDisclosure, Button } from '@chakra-ui/react';
import MyModal from '@/components/MyModal';
export const useEditTitle = ({
title,
placeholder = ''
}: {
title: string;
placeholder?: string;
}) => {
const { isOpen, onOpen, onClose } = useDisclosure();
const inputRef = useRef<HTMLInputElement | null>(null);
const onSuccessCb = useRef<(content: string) => void | Promise<void>>();
const onErrorCb = useRef<(err: any) => void>();
const defaultValue = useRef('');
const onOpenModal = useCallback(
({
defaultVal,
onSuccess,
onError
}: {
defaultVal: string;
onSuccess: (content: string) => any;
onError?: (err: any) => void;
}) => {
onOpen();
onSuccessCb.current = onSuccess;
onErrorCb.current = onError;
defaultValue.current = defaultVal;
},
[onOpen]
);
const onclickConfirm = useCallback(async () => {
if (!inputRef.current) return;
try {
const val = inputRef.current.value;
await onSuccessCb.current?.(val);
onClose();
} catch (err) {
onErrorCb.current?.(err);
}
}, [onClose]);
// eslint-disable-next-line react/display-name
const EditModal = useCallback(
() => (
<MyModal isOpen={isOpen} onClose={onClose} title={title}>
<ModalBody>
<Input
ref={inputRef}
defaultValue={defaultValue.current}
placeholder={placeholder}
autoFocus
maxLength={20}
/>
</ModalBody>
<ModalFooter>
<Button mr={3} variant={'base'} onClick={onClose}>
取消
</Button>
<Button onClick={onclickConfirm}>确认</Button>
</ModalFooter>
</MyModal>
),
[isOpen, onClose, onclickConfirm, placeholder, title]
);
return {
onOpenModal,
EditModal
};
};
import { useState, useCallback } from 'react';
import LoadingComponent from '@/components/Loading';
export const useLoading = (props?: { defaultLoading: boolean }) => {
const [isLoading, setIsLoading] = useState(props?.defaultLoading || false);
const Loading = useCallback(
({
loading,
fixed = true,
text = ''
}: {
loading?: boolean;
fixed?: boolean;
text?: string;
}): JSX.Element | null => {
return isLoading || loading ? <LoadingComponent fixed={fixed} text={text} /> : null;
},
[isLoading]
);
return {
isLoading,
setIsLoading,
Loading
};
};
import { useToast } from '@/hooks/useToast';
import { useMutation } from '@tanstack/react-query';
import type { UseMutationOptions } from '@tanstack/react-query';
import { getErrText } from '@/utils/tools';
import { useTranslation } from 'react-i18next';
interface Props extends UseMutationOptions<any, any, any, any> {
successToast?: string | null;
errorToast?: string | null;
}
export const useRequest = ({ successToast, errorToast, onSuccess, onError, ...props }: Props) => {
const { toast } = useToast();
const { t } = useTranslation();
const mutation = useMutation<unknown, unknown, any, unknown>({
...props,
onSuccess(res, variables: void, context: unknown) {
onSuccess?.(res, variables, context);
successToast &&
toast({
title: successToast,
status: 'success'
});
},
onError(err: any, variables: void, context: unknown) {
onError?.(err, variables, context);
errorToast &&
toast({
title: t(getErrText(err, errorToast)),
status: 'error'
});
}
});
return mutation;
};
import React, { useRef, useCallback } from 'react';
import { Box } from '@chakra-ui/react';
import { useToast } from './useToast';
import { useTranslation } from 'react-i18next';
export const useSelectFile = (props?: { fileType?: string; multiple?: boolean }) => {
const { t } = useTranslation();
const { fileType = '*', multiple = false } = props || {};
const { toast } = useToast();
const SelectFileDom = useRef<HTMLInputElement>(null);
const File = useCallback(
({ onSelect }: { onSelect: (e: File[]) => void }) => (
<Box position={'absolute'} w={0} h={0} overflow={'hidden'}>
<input
ref={SelectFileDom}
type="file"
accept={fileType}
multiple={multiple}
onChange={(e) => {
if (!e.target.files || e.target.files?.length === 0) return;
if (e.target.files.length > 10) {
return toast({
status: 'warning',
title: t('file.Select a maximum of 10 files')
});
}
onSelect(Array.from(e.target.files));
}}
/>
</Box>
),
[fileType, multiple, t, toast]
);
const onOpen = useCallback(() => {
SelectFileDom.current && SelectFileDom.current.click();
}, []);
return {
File,
onOpen
};
};
import { useState, useMemo, useCallback } from 'react';
import { sendAuthCode } from '@/api/user';
import { UserAuthTypeEnum } from '@/constants/common';
import { useToast } from './useToast';
import { feConfigs } from '@/store/static';
import { getErrText } from '@/utils/tools';
let timer: any;
export const useSendCode = () => {
const { toast } = useToast();
const [codeSending, setCodeSending] = useState(false);
const [codeCountDown, setCodeCountDown] = useState(0);
const sendCodeText = useMemo(() => {
if (codeCountDown >= 10) {
return `${codeCountDown}s后重新获取`;
}
if (codeCountDown > 0) {
return `0${codeCountDown}s后重新获取`;
}
return '获取验证码';
}, [codeCountDown]);
const sendCode = useCallback(
async ({ username, type }: { username: string; type: `${UserAuthTypeEnum}` }) => {
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);
},
[toast]
);
return {
codeSending,
sendCode,
sendCodeText,
codeCountDown
};
};
export function getClientToken(googleClientVerKey?: string) {
if (!googleClientVerKey || typeof window.grecaptcha === 'undefined' || !window.grecaptcha?.ready)
return '';
return new Promise<string>((resolve, reject) => {
window.grecaptcha.ready(async () => {
try {
const token = await window.grecaptcha.execute(googleClientVerKey, {
action: 'submit'
});
resolve(token);
} catch (error) {
reject(error);
}
});
});
}
import { useEffect, useState } from 'react';
import type { AppProps } from 'next/app';
import Script from 'next/script';
import Head from 'next/head';
import { ChakraProvider, ColorModeScript } from '@chakra-ui/react';
import Layout from '@/components/Layout';
import { theme } from '@/constants/theme';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import NProgress from 'nprogress'; //nprogress module
import Router from 'next/router';
import { clientInitData, feConfigs } from '@/store/static';
import { appWithTranslation, useTranslation } from 'next-i18next';
import { getLangStore, setLangStore } from '@/utils/web/i18n';
import { useRouter } from 'next/router';
import { useGlobalStore } from '@/store/global';
import 'nprogress/nprogress.css';
import '@/styles/reset.scss';
import { FeConfigsType } from '@/types';
//Binding events.
Router.events.on('routeChangeStart', () => NProgress.start());
Router.events.on('routeChangeComplete', () => NProgress.done());
Router.events.on('routeChangeError', () => NProgress.done());
// Create a client
const queryClient = new QueryClient({
defaultOptions: {
queries: {
refetchOnWindowFocus: false,
retry: false,
cacheTime: 10
}
}
});
function App({ Component, pageProps }: AppProps) {
const router = useRouter();
const { hiId } = router.query as { hiId?: string };
const { i18n } = useTranslation();
const { setLastRoute } = useGlobalStore();
const [scripts, setScripts] = useState<FeConfigsType['scripts']>([]);
useEffect(() => {
// get init data
(async () => {
const {
feConfigs: { scripts }
} = await clientInitData();
setScripts(scripts || []);
})();
// add window error track
window.onerror = function (msg, url) {
window.umami?.track('windowError', {
device: {
userAgent: navigator.userAgent,
platform: navigator.platform,
appName: navigator.appName
},
msg,
url
});
};
return () => {
window.onerror = null;
};
}, []);
useEffect(() => {
hiId && localStorage.setItem('inviterId', hiId);
}, [hiId]);
useEffect(() => {
const lang = getLangStore() || 'zh';
i18n?.changeLanguage?.(lang);
setLangStore(lang);
return () => {
setLastRoute(router.asPath);
};
}, [router.asPath]);
return (
<>
<Head>
<title>{feConfigs?.systemTitle || 'AI'}</title>
<meta
name="description"
content="FastGPT is a knowledge-based question answering system built on the LLM. It offers out-of-the-box data processing and model invocation capabilities. Moreover, it allows for workflow orchestration through Flow visualization, thereby enabling complex question and answer scenarios!"
/>
<meta
name="viewport"
content="width=device-width,initial-scale=1.0,maximum-scale=1.0,minimum-scale=1.0,user-scalable=no, viewport-fit=cover"
/>
<link rel="icon" href="/favicon.ico" />
</Head>
{scripts?.map((item, i) => (
<Script key={i} strategy="lazyOnload" {...item}></Script>
))}
<QueryClientProvider client={queryClient}>
<ChakraProvider theme={theme}>
<ColorModeScript initialColorMode={theme.config.initialColorMode} />
<Layout>
<Component {...pageProps} />
</Layout>
</ChakraProvider>
</QueryClientProvider>
</>
);
}
export default appWithTranslation(App);
import { useEffect } from 'react';
import { useRouter } from 'next/router';
import { serviceSideProps } from '@/utils/web/i18n';
import { useGlobalStore } from '@/store/global';
import { addLog } from '@/service/utils/tools';
import { getErrText } from '@/utils/tools';
function Error() {
const router = useRouter();
const { lastRoute } = useGlobalStore();
useEffect(() => {
setTimeout(() => {
window.umami?.track('pageError', {
userAgent: navigator.userAgent,
platform: navigator.platform,
appName: navigator.appName,
lastRoute,
route: router.asPath
});
}, 1000);
setTimeout(() => {
router.back();
}, 2000);
}, []);
return (
<p>
部分系统不兼容,导致页面崩溃。如果可以,请联系作者,反馈下具体操作和页面。大部分是 苹果 的
safari 浏览器导致,可以尝试更换 chrome 浏览器。
</p>
);
}
export async function getServerSideProps(context: any) {
console.log('[render error]: ', context);
addLog.error(getErrText(context?.res));
return {
props: { ...(await serviceSideProps(context)) }
};
}
export default Error;
import React, { useMemo } from 'react';
import {
ModalBody,
Flex,
Box,
Table,
Thead,
Tbody,
Tr,
Th,
Td,
TableContainer
} from '@chakra-ui/react';
import { UserBillType } from '@/types/user';
import dayjs from 'dayjs';
import { BillSourceMap } from '@/constants/user';
import { formatPrice } from '@/utils/user';
import MyModal from '@/components/MyModal';
import { useTranslation } from 'react-i18next';
const BillDetail = ({ bill, onClose }: { bill: UserBillType; onClose: () => void }) => {
const { t } = useTranslation();
const filterBillList = useMemo(
() => bill.list.filter((item) => item && item.moduleName),
[bill.list]
);
return (
<MyModal isOpen={true} onClose={onClose} title={t('user.Bill Detail')}>
<ModalBody>
<Flex alignItems={'center'} pb={4}>
<Box flex={'0 0 80px'}>订单号:</Box>
<Box>{bill.id}</Box>
</Flex>
<Flex alignItems={'center'} pb={4}>
<Box flex={'0 0 80px'}>生成时间:</Box>
<Box>{dayjs(bill.time).format('YYYY/MM/DD HH:mm:ss')}</Box>
</Flex>
<Flex alignItems={'center'} pb={4}>
<Box flex={'0 0 80px'}>应用名:</Box>
<Box>{t(bill.appName) || '-'}</Box>
</Flex>
<Flex alignItems={'center'} pb={4}>
<Box flex={'0 0 80px'}>来源:</Box>
<Box>{BillSourceMap[bill.source]}</Box>
</Flex>
<Flex alignItems={'center'} pb={4}>
<Box flex={'0 0 80px'}>总金额:</Box>
<Box fontWeight={'bold'}>{bill.total}</Box>
</Flex>
<Box pb={4}>
<Box flex={'0 0 80px'} mb={1}>
扣费模块
</Box>
<TableContainer>
<Table>
<Thead>
<Tr>
<Th>模块名</Th>
<Th>AI模型</Th>
<Th>Token长度</Th>
<Th>费用</Th>
</Tr>
</Thead>
<Tbody>
{filterBillList.map((item, i) => (
<Tr key={i}>
<Td>{item.moduleName}</Td>
<Td>{item.model}</Td>
<Td>{item.tokenLen}</Td>
<Td>{formatPrice(item.amount)}</Td>
</Tr>
))}
</Tbody>
</Table>
</TableContainer>
</Box>
</ModalBody>
</MyModal>
);
};
export default BillDetail;
import React, { useState } from 'react';
import {
Table,
Thead,
Tbody,
Tr,
Th,
Td,
TableContainer,
Flex,
Box,
Button
} from '@chakra-ui/react';
import { BillSourceMap } from '@/constants/user';
import { getUserBills } from '@/api/user';
import type { UserBillType } from '@/types/user';
import { usePagination } from '@/hooks/usePagination';
import { useLoading } from '@/hooks/useLoading';
import dayjs from 'dayjs';
import MyIcon from '@/components/Icon';
import DateRangePicker, { type DateRangeType } from '@/components/DateRangePicker';
import { addDays } from 'date-fns';
import dynamic from 'next/dynamic';
import { useGlobalStore } from '@/store/global';
import { useTranslation } from 'next-i18next';
const BillDetail = dynamic(() => import('./BillDetail'));
const BillTable = () => {
const { t } = useTranslation();
const { Loading } = useLoading();
const [dateRange, setDateRange] = useState<DateRangeType>({
from: addDays(new Date(), -7),
to: new Date()
});
const { isPc } = useGlobalStore();
const {
data: bills,
isLoading,
Pagination,
getData
} = usePagination<UserBillType>({
api: getUserBills,
pageSize: isPc ? 20 : 10,
params: {
dateStart: dateRange.from || new Date(),
dateEnd: addDays(dateRange.to || new Date(), 1)
}
});
const [billDetail, setBillDetail] = useState<UserBillType>();
return (
<Flex flexDirection={'column'} py={[0, 5]} h={'100%'} position={'relative'}>
<TableContainer px={[3, 8]} position={'relative'} flex={'1 0 0'} h={0} overflowY={'auto'}>
<Table>
<Thead>
<Tr>
<Th>{t('user.Time')}</Th>
<Th>{t('user.Source')}</Th>
<Th>{t('user.Application Name')}</Th>
<Th>{t('user.Total Amount')}</Th>
<Th></Th>
</Tr>
</Thead>
<Tbody fontSize={'sm'}>
{bills.map((item) => (
<Tr key={item.id}>
<Td>{dayjs(item.time).format('YYYY/MM/DD HH:mm:ss')}</Td>
<Td>{BillSourceMap[item.source]}</Td>
<Td>{t(item.appName) || '-'}</Td>
<Td>{item.total}</Td>
<Td>
<Button size={'sm'} variant={'base'} onClick={() => setBillDetail(item)}>
详情
</Button>
</Td>
</Tr>
))}
</Tbody>
</Table>
</TableContainer>
{!isLoading && bills.length === 0 && (
<Flex flex={'1 0 0'} flexDirection={'column'} alignItems={'center'}>
<MyIcon name="empty" w={'48px'} h={'48px'} color={'transparent'} />
<Box mt={2} color={'myGray.500'}>
无使用记录~
</Box>
</Flex>
)}
<Flex w={'100%'} mt={4} px={[3, 8]} alignItems={'center'} justifyContent={'flex-end'}>
<DateRangePicker
defaultDate={dateRange}
position="top"
onChange={setDateRange}
onSuccess={() => getData(1)}
/>
<Box ml={3}>
<Pagination />
</Box>
</Flex>
<Loading loading={isLoading} fixed={false} />
{!!billDetail && <BillDetail bill={billDetail} onClose={() => setBillDetail(undefined)} />}
</Flex>
);
};
export default BillTable;
import React from 'react';
import { Box, Flex, useTheme } from '@chakra-ui/react';
import { getInforms, readInform } from '@/api/user';
import { usePagination } from '@/hooks/usePagination';
import { useLoading } from '@/hooks/useLoading';
import type { informSchema } from '@/types/mongoSchema';
import { formatTimeToChatTime } from '@/utils/tools';
import { useGlobalStore } from '@/store/global';
import MyIcon from '@/components/Icon';
const BillTable = () => {
const theme = useTheme();
const { Loading } = useLoading();
const { isPc } = useGlobalStore();
const {
data: informs,
isLoading,
total,
pageSize,
Pagination,
getData,
pageNum
} = usePagination<informSchema>({
api: getInforms,
pageSize: isPc ? 20 : 10
});
return (
<Flex flexDirection={'column'} py={[0, 5]} h={'100%'} position={'relative'}>
<Box px={[3, 8]} position={'relative'} flex={'1 0 0'} h={0} overflowY={'auto'}>
{informs.map((item) => (
<Box
key={item._id}
border={theme.borders.md}
py={2}
px={4}
borderRadius={'md'}
cursor={item.read ? 'default' : 'pointer'}
position={'relative'}
_notLast={{ mb: 3 }}
onClick={async () => {
if (!item.read) {
await readInform(item._id);
getData(pageNum);
}
}}
>
<Flex alignItems={'center'} justifyContent={'space-between'}>
<Box>{item.title}</Box>
<Box ml={2} color={'myGray.500'}>
{formatTimeToChatTime(item.time)}
</Box>
</Flex>
<Box fontSize={'sm'} color={'myGray.600'}>
{item.content}
</Box>
{!item.read && (
<Box
w={'5px'}
h={'5px'}
borderRadius={'10px'}
bg={'myRead.600'}
position={'absolute'}
bottom={'8px'}
right={'8px'}
></Box>
)}
</Box>
))}
</Box>
{!isLoading && informs.length === 0 && (
<Flex flex={'1 0 0'} flexDirection={'column'} alignItems={'center'} pt={'-48px'}>
<MyIcon name="empty" w={'48px'} h={'48px'} color={'transparent'} />
<Box mt={2} color={'myGray.500'}>
暂无通知~
</Box>
</Flex>
)}
{total > pageSize && (
<Flex w={'100%'} mt={4} px={[3, 8]} justifyContent={'flex-end'}>
<Pagination />
</Flex>
)}
<Loading loading={isLoading && informs.length === 0} fixed={false} />
</Flex>
);
};
export default BillTable;
import React from 'react';
import { ModalBody, Box, Flex, Input, ModalFooter, Button } from '@chakra-ui/react';
import MyModal from '@/components/MyModal';
import { useTranslation } from 'react-i18next';
import { useForm } from 'react-hook-form';
import { useRequest } from '@/hooks/useRequest';
import { UserType } from '@/types/user';
const OpenAIAccountModal = ({
defaultData,
onSuccess,
onClose
}: {
defaultData: UserType['openaiAccount'];
onSuccess: (e: UserType['openaiAccount']) => Promise<any>;
onClose: () => void;
}) => {
const { t } = useTranslation();
const { register, handleSubmit } = useForm({
defaultValues: defaultData
});
const { mutate: onSubmit, isLoading } = useRequest({
mutationFn: async (data: UserType['openaiAccount']) => onSuccess(data),
onSuccess(res) {
onClose();
},
errorToast: t('user.Set OpenAI Account Failed')
});
return (
<MyModal isOpen onClose={onClose} title={t('user.OpenAI Account Setting')}>
<ModalBody>
<Box fontSize={'sm'} color={'myGray.500'}>
如果你填写了该内容,在线上平台使用 OpenAI Chat 模型不会计费(不包含知识库训练、索引生成)
</Box>
<Flex alignItems={'center'} mt={5}>
<Box flex={'0 0 65px'}>API Key:</Box>
<Input flex={1} {...register('key')}></Input>
</Flex>
<Flex alignItems={'center'} mt={5}>
<Box flex={'0 0 65px'}>BaseUrl:</Box>
<Input
flex={1}
{...register('baseUrl')}
placeholder={'请求地址,默认为 openai 官方。可填中转地址,未自动补全 "v1"'}
></Input>
</Flex>
</ModalBody>
<ModalFooter>
<Button mr={3} variant={'base'} onClick={onClose}>
取消
</Button>
<Button isLoading={isLoading} onClick={handleSubmit((data) => onSubmit(data))}>
确认
</Button>
</ModalFooter>
</MyModal>
);
};
export default OpenAIAccountModal;
import React, { useState, useCallback } from 'react';
import { ModalFooter, ModalBody, Button, Input, Box, Grid } from '@chakra-ui/react';
import { getPayCode, checkPayResult } from '@/api/user';
import { useToast } from '@/hooks/useToast';
import { useQuery } from '@tanstack/react-query';
import { useRouter } from 'next/router';
import { getErrText } from '@/utils/tools';
import { useTranslation } from 'react-i18next';
import { formatPrice } from '@/utils/user';
import Markdown from '@/components/Markdown';
import MyModal from '@/components/MyModal';
import { vectorModelList, chatModelList, qaModel } from '@/store/static';
const PayModal = ({ onClose }: { onClose: () => void }) => {
const router = useRouter();
const { t } = useTranslation();
const { toast } = useToast();
const [inputVal, setInputVal] = useState<number | ''>('');
const [loading, setLoading] = useState(false);
const [payId, setPayId] = useState('');
const handleClickPay = useCallback(async () => {
if (!inputVal || inputVal <= 0 || isNaN(+inputVal)) return;
setLoading(true);
try {
// 获取支付二维码
const res = await getPayCode(inputVal);
new window.QRCode(document.getElementById('payQRCode'), {
text: res.codeUrl,
width: 128,
height: 128,
colorDark: '#000000',
colorLight: '#ffffff',
correctLevel: window.QRCode.CorrectLevel.H
});
setPayId(res.payId);
} catch (err) {
toast({
title: getErrText(err),
status: 'error'
});
}
setLoading(false);
}, [inputVal, toast]);
useQuery(
[payId],
() => {
if (!payId) return null;
return checkPayResult(payId);
},
{
enabled: !!payId,
refetchInterval: 3000,
onSuccess(res) {
if (!res) return;
toast({
title: '充值成功',
status: 'success'
});
router.reload();
}
}
);
return (
<MyModal isOpen={true} onClose={payId ? onClose : undefined} title={t('user.Pay')} isCentered>
<ModalBody py={0}>
{!payId && (
<>
<Grid gridTemplateColumns={'repeat(4,1fr)'} gridGap={5} mb={4}>
{[10, 20, 50, 100].map((item) => (
<Button
key={item}
variant={item === inputVal ? 'solid' : 'outline'}
onClick={() => setInputVal(item)}
>
{item}
</Button>
))}
</Grid>
<Box mb={4}>
<Input
value={inputVal}
type={'number'}
step={1}
placeholder={'其他金额,请取整数'}
onChange={(e) => {
setInputVal(Math.floor(+e.target.value));
}}
></Input>
</Box>
<Markdown
source={`
| 计费项 | 价格: 元/ 1K tokens(包含上下文)|
| --- | --- |
${vectorModelList
.map((item) => `| 索引-${item.name} | ${formatPrice(item.price, 1000)} |`)
.join('\n')}
${chatModelList
.map((item) => `| 对话-${item.name} | ${formatPrice(item.price, 1000)} |`)
.join('\n')}
| 文件QA拆分 | ${formatPrice(qaModel.price, 1000)} |`}
/>
</>
)}
{/* 付费二维码 */}
<Box textAlign={'center'}>
{payId && <Box mb={3}>请微信扫码支付: {inputVal}元,请勿关闭页面</Box>}
<Box id={'payQRCode'} display={'inline-block'}></Box>
</Box>
</ModalBody>
<ModalFooter>
{!payId && (
<>
<Button variant={'base'} onClick={onClose}>
取消
</Button>
<Button
ml={3}
isLoading={loading}
isDisabled={!inputVal || inputVal === 0}
onClick={handleClickPay}
>
获取充值二维码
</Button>
</>
)}
</ModalFooter>
</MyModal>
);
};
export default PayModal;
import React, { useState, useCallback } from 'react';
import {
Button,
Table,
Thead,
Tbody,
Tr,
Th,
Td,
TableContainer,
Flex,
Box
} from '@chakra-ui/react';
import { getPayOrders, checkPayResult } from '@/api/user';
import { PaySchema } from '@/types/mongoSchema';
import dayjs from 'dayjs';
import { useQuery } from '@tanstack/react-query';
import { formatPrice } from '@/utils/user';
import { useGlobalStore } from '@/store/global';
import { useToast } from '@/hooks/useToast';
import { useLoading } from '@/hooks/useLoading';
import MyIcon from '@/components/Icon';
const PayRecordTable = () => {
const { Loading, setIsLoading } = useLoading();
const [payOrders, setPayOrders] = useState<PaySchema[]>([]);
const { toast } = useToast();
const { isInitialLoading, refetch } = useQuery(['initPayOrder'], getPayOrders, {
onSuccess(res) {
setPayOrders(res);
}
});
const handleRefreshPayOrder = useCallback(
async (payId: string) => {
setIsLoading(true);
try {
const data = await checkPayResult(payId);
toast({
title: data,
status: 'success'
});
} catch (error: any) {
toast({
title: error?.message,
status: 'warning'
});
console.log(error);
}
try {
refetch();
} catch (error) {}
setIsLoading(false);
},
[refetch, setIsLoading, toast]
);
return (
<Box position={'relative'} h={'100%'}>
{!isInitialLoading && payOrders.length === 0 ? (
<Flex h={'100%'} flexDirection={'column'} alignItems={'center'} justifyContent={'center'}>
<MyIcon name="empty" w={'48px'} h={'48px'} color={'transparent'} />
<Box mt={2} color={'myGray.500'}>
无支付记录~
</Box>
</Flex>
) : (
<TableContainer py={[0, 5]} px={[3, 8]} h={'100%'} overflow={'overlay'}>
<Table>
<Thead>
<Tr>
<Th>订单号</Th>
<Th>时间</Th>
<Th>金额</Th>
<Th>状态</Th>
<Th></Th>
</Tr>
</Thead>
<Tbody fontSize={'sm'}>
{payOrders.map((item) => (
<Tr key={item._id}>
<Td>{item.orderId}</Td>
<Td>
{item.createTime ? dayjs(item.createTime).format('YYYY/MM/DD HH:mm:ss') : '-'}
</Td>
<Td>{formatPrice(item.price)}</Td>
<Td>{item.status}</Td>
<Td>
{item.status === 'NOTPAY' && (
<Button onClick={() => handleRefreshPayOrder(item._id)} size={'sm'}>
更新
</Button>
)}
</Td>
</Tr>
))}
</Tbody>
</Table>
</TableContainer>
)}
<Loading loading={isInitialLoading} fixed={false} />
</Box>
);
};
export default PayRecordTable;
import React from 'react';
import {
Grid,
Box,
Flex,
BoxProps,
useTheme,
Button,
Table,
Thead,
Tbody,
Tr,
Th,
Td,
TableContainer
} from '@chakra-ui/react';
import { useTranslation } from 'next-i18next';
import { useQuery } from '@tanstack/react-query';
import { getPromotionInitData, getPromotionRecords } from '@/api/user';
import { useUserStore } from '@/store/user';
import { useLoading } from '@/hooks/useLoading';
import MyTooltip from '@/components/MyTooltip';
import { QuestionOutlineIcon } from '@chakra-ui/icons';
import { useCopyData } from '@/hooks/useCopyData';
import { usePagination } from '@/hooks/usePagination';
import { PromotionRecordType } from '@/api/response/user';
import MyIcon from '@/components/Icon';
import dayjs from 'dayjs';
const Promotion = () => {
const { t } = useTranslation();
const theme = useTheme();
const { copyData } = useCopyData();
const { userInfo } = useUserStore();
const { Loading } = useLoading();
const {
data: promotionRecords,
isLoading,
total,
pageSize,
Pagination
} = usePagination<PromotionRecordType>({
api: getPromotionRecords
});
const { data: { invitedAmount = 0, earningsAmount = 0 } = {} } = useQuery(
['getPromotionInitData'],
getPromotionInitData
);
const statisticsStyles: BoxProps = {
p: [4, 5],
border: theme.borders.base,
textAlign: 'center',
fontSize: ['md', 'xl'],
borderRadius: 'md'
};
const titleStyles: BoxProps = {
mt: 2,
fontSize: ['lg', '28px'],
fontWeight: 'bold'
};
return (
<Flex flexDirection={'column'} py={[0, 5]} px={5} h={'100%'} position={'relative'}>
<Grid gridTemplateColumns={['1fr 1fr', 'repeat(2,1fr)', 'repeat(4,1fr)']} gridGap={5}>
<Box {...statisticsStyles}>
<Box>{t('user.Amount of inviter')}</Box>
<Box {...titleStyles}>{invitedAmount}</Box>
</Box>
<Box {...statisticsStyles}>
<Box>{t('user.Amount of earnings')}</Box>
<Box {...titleStyles}>{earningsAmount}</Box>
</Box>
<Box {...statisticsStyles}>
<Flex alignItems={'center'} justifyContent={'center'}>
<Box>{t('user.Promotion Rate')}</Box>
<MyTooltip label={t('user.Promotion rate tip')}>
<QuestionOutlineIcon ml={1} />
</MyTooltip>
</Flex>
<Box {...titleStyles}>{userInfo?.promotionRate || 15}%</Box>
</Box>
<Box {...statisticsStyles}>
<Flex alignItems={'center'} justifyContent={'center'}>
<Box>{t('user.Invite Url')}</Box>
<MyTooltip label={t('user.Invite url tip')}>
<QuestionOutlineIcon ml={1} />
</MyTooltip>
</Flex>
<Button
mt={4}
variant={'base'}
fontSize={'sm'}
onClick={() => {
copyData(`${location.origin}/?hiId=${userInfo?._id}`);
}}
>
{t('user.Copy invite url')}
</Button>
</Box>
</Grid>
<Box mt={5}>
<TableContainer position={'relative'} overflow={'hidden'} minH={'100px'}>
<Table>
<Thead>
<Tr>
<Th>时间</Th>
<Th>类型</Th>
<Th>金额(¥)</Th>
</Tr>
</Thead>
<Tbody fontSize={'sm'}>
{promotionRecords.map((item) => (
<Tr key={item._id}>
<Td>
{item.createTime ? dayjs(item.createTime).format('YYYY/MM/DD HH:mm:ss') : '-'}
</Td>
<Td>{t(`user.promotion.${item.type}`)}</Td>
<Td>{item.amount}</Td>
</Tr>
))}
</Tbody>
</Table>
</TableContainer>
{!isLoading && promotionRecords.length === 0 && (
<Flex mt={'10vh'} flexDirection={'column'} alignItems={'center'}>
<MyIcon name="empty" w={'48px'} h={'48px'} color={'transparent'} />
<Box mt={2} color={'myGray.500'}>
无邀请记录~
</Box>
</Flex>
)}
{total > pageSize && (
<Flex mt={4} justifyContent={'flex-end'}>
<Pagination />
</Flex>
)}
<Loading loading={isLoading} fixed={false} />
</Box>
</Flex>
);
};
export default Promotion;
import React from 'react';
import { ModalBody, Box, Flex, Input, ModalFooter, Button } from '@chakra-ui/react';
import MyModal from '@/components/MyModal';
import { useTranslation } from 'react-i18next';
import { useForm } from 'react-hook-form';
import { useRequest } from '@/hooks/useRequest';
import { updatePasswordByOld } from '@/api/user';
type FormType = {
oldPsw: string;
newPsw: string;
confirmPsw: string;
};
const UpdatePswModal = ({ onClose }: { onClose: () => void }) => {
const { t } = useTranslation();
const { register, handleSubmit } = useForm<FormType>({
defaultValues: {
oldPsw: '',
newPsw: '',
confirmPsw: ''
}
});
const { mutate: onSubmit, isLoading } = useRequest({
mutationFn: (data: FormType) => {
if (data.newPsw !== data.confirmPsw) {
return Promise.reject(t('common.Password inconsistency'));
}
return updatePasswordByOld(data);
},
onSuccess() {
onClose();
},
successToast: t('user.Update password succseful'),
errorToast: t('user.Update password failed')
});
return (
<MyModal isOpen onClose={onClose} title={t('user.Update Password')}>
<ModalBody>
<Flex alignItems={'center'}>
<Box flex={'0 0 70px'}>旧密码:</Box>
<Input flex={1} type={'password'} {...register('oldPsw', { required: true })}></Input>
</Flex>
<Flex alignItems={'center'} mt={5}>
<Box flex={'0 0 70px'}>新密码:</Box>
<Input
flex={1}
type={'password'}
{...register('newPsw', {
required: true,
maxLength: {
value: 20,
message: '密码最少 4 位最多 20 位'
}
})}
></Input>
</Flex>
<Flex alignItems={'center'} mt={5}>
<Box flex={'0 0 70px'}>确认密码:</Box>
<Input
flex={1}
type={'password'}
{...register('confirmPsw', {
required: true,
maxLength: {
value: 20,
message: '密码最少 4 位最多 20 位'
}
})}
></Input>
</Flex>
</ModalBody>
<ModalFooter>
<Button mr={3} variant={'base'} onClick={onClose}>
取消
</Button>
<Button isLoading={isLoading} onClick={handleSubmit((data) => onSubmit(data))}>
确认
</Button>
</ModalFooter>
</MyModal>
);
};
export default UpdatePswModal;
import React, { useCallback, useRef } from 'react';
import { Box, Flex, useTheme } from '@chakra-ui/react';
import { useGlobalStore } from '@/store/global';
import { useRouter } from 'next/router';
import dynamic from 'next/dynamic';
import { clearToken } from '@/utils/user';
import { useUserStore } from '@/store/user';
import { useConfirm } from '@/hooks/useConfirm';
import PageContainer from '@/components/PageContainer';
import SideTabs from '@/components/SideTabs';
import Tabs from '@/components/Tabs';
import UserInfo from './components/Info';
import { serviceSideProps } from '@/utils/web/i18n';
import { feConfigs } from '@/store/static';
import { useTranslation } from 'react-i18next';
import Script from 'next/script';
const Promotion = dynamic(() => import('./components/Promotion'));
const BillTable = dynamic(() => import('./components/BillTable'));
const PayRecordTable = dynamic(() => import('./components/PayRecordTable'));
const InformTable = dynamic(() => import('./components/InformTable'));
const ApiKeyTable = dynamic(() => import('./components/ApiKeyTable'));
enum TabEnum {
'info' = 'info',
'promotion' = 'promotion',
'bill' = 'bill',
'pay' = 'pay',
'inform' = 'inform',
'apikey' = 'apikey',
'loginout' = 'loginout'
}
const Account = ({ currentTab }: { currentTab: `${TabEnum}` }) => {
const { t } = useTranslation();
const tabList = [
{
icon: 'meLight',
label: t('user.Personal Information'),
id: TabEnum.info
},
{
icon: 'billRecordLight',
label: t('user.Usage Record'),
id: TabEnum.bill
},
...(feConfigs?.show_promotion
? [
{
icon: 'promotionLight',
label: t('user.Promotion Record'),
id: TabEnum.promotion
}
]
: []),
...(feConfigs?.show_pay
? [
{
icon: 'payRecordLight',
label: t('user.Recharge Record'),
id: TabEnum.pay
}
]
: []),
{
icon: 'apikey',
label: t('user.apikey.key'),
id: TabEnum.apikey
},
{
icon: 'informLight',
label: t('user.Notice'),
id: TabEnum.inform
},
{
icon: 'loginoutLight',
label: t('user.Sign Out'),
id: TabEnum.loginout
}
];
const { openConfirm, ConfirmModal } = useConfirm({
content: '确认退出登录?'
});
const router = useRouter();
const theme = useTheme();
const { isPc } = useGlobalStore();
const { setUserInfo } = useUserStore();
const setCurrentTab = useCallback(
(tab: string) => {
if (tab === TabEnum.loginout) {
openConfirm(() => {
clearToken();
setUserInfo(null);
router.replace('/login');
})();
} else {
router.replace({
query: {
currentTab: tab
}
});
}
},
[openConfirm, router, setUserInfo]
);
return (
<>
<Script src="/js/qrcode.min.js" strategy="lazyOnload"></Script>
<PageContainer>
<Flex flexDirection={['column', 'row']} h={'100%'} pt={[4, 0]}>
{isPc ? (
<Flex
flexDirection={'column'}
p={4}
h={'100%'}
flex={'0 0 200px'}
borderRight={theme.borders.base}
>
<SideTabs
flex={1}
mx={'auto'}
mt={2}
w={'100%'}
list={tabList}
activeId={currentTab}
onChange={setCurrentTab}
/>
</Flex>
) : (
<Box mb={3}>
<Tabs
m={'auto'}
size={isPc ? 'md' : 'sm'}
list={tabList.map((item) => ({
id: item.id,
label: item.label
}))}
activeId={currentTab}
onChange={setCurrentTab}
/>
</Box>
)}
<Box flex={'1 0 0'} h={'100%'} pb={[4, 0]}>
{currentTab === TabEnum.info && <UserInfo />}
{currentTab === TabEnum.promotion && <Promotion />}
{currentTab === TabEnum.bill && <BillTable />}
{currentTab === TabEnum.pay && <PayRecordTable />}
{currentTab === TabEnum.inform && <InformTable />}
{currentTab === TabEnum.apikey && <ApiKeyTable />}
</Box>
</Flex>
<ConfirmModal />
</PageContainer>
</>
);
};
export async function getServerSideProps(content: any) {
return {
props: {
currentTab: content?.query?.currentTab || TabEnum.info,
...(await serviceSideProps(content))
}
};
}
export default Account;
// 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 } from '@/service/mongo';
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try {
await authUser({ req, authRoot: true });
await connectToDatabase();
const { limit = 1000 } = req.body as { limit: number };
let skip = 0;
const total = await Chat.countDocuments({
chatId: { $exists: false }
});
let promise = Promise.resolve();
console.log(total);
for (let i = 0; i < total; i += limit) {
const skipVal = skip;
skip += limit;
promise = promise
.then(() => init(limit, skipVal))
.then(() => {
console.log(skipVal);
});
}
await promise;
jsonRes(res, {});
} catch (error) {
jsonRes(res, {
code: 500,
error
});
}
}
async function init(limit: number, skip: number) {
// 遍历 app
const chats = await Chat.find(
{
chatId: { $exists: false }
},
'_id'
).limit(limit);
await Promise.all(
chats.map((chat) =>
Chat.findByIdAndUpdate(chat._id, {
chatId: String(chat._id),
source: 'online'
})
)
);
}
// 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 });
}
})
);
}
// 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, OutLink } from '@/service/mongo';
import { OutLinkTypeEnum } from '@/constants/chat';
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try {
await authUser({ req, authRoot: true });
await connectToDatabase();
await OutLink.updateMany(
{},
{
$set: { type: OutLinkTypeEnum.share }
}
);
jsonRes(res, {});
} catch (error) {
jsonRes(res, {
code: 500,
error
});
}
}
// 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 { PgClient } from '@/service/pg';
import { PgDatasetTableName } from '@/constants/plugin';
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try {
await authUser({ req, authRoot: true });
const { rowCount } = await PgClient.query(`SELECT 1
FROM information_schema.columns
WHERE table_schema = 'public'
AND table_name = '${PgDatasetTableName}'
AND column_name = 'file_id'`);
if (rowCount > 0) {
return jsonRes(res, {
data: '已经存在file_id字段'
});
}
jsonRes(res, {
data: await PgClient.query(
`ALTER TABLE ${PgDatasetTableName} ADD COLUMN file_id VARCHAR(100)`
)
});
} catch (error) {
jsonRes(res, {
code: 500,
error
});
}
}
// 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, KB } from '@/service/mongo';
import { KbTypeEnum } from '@/constants/dataset';
import { PgClient } from '@/service/pg';
import { PgDatasetTableName } from '@/constants/plugin';
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try {
await connectToDatabase();
await authUser({ req, authRoot: true });
await KB.updateMany(
{
type: { $exists: false }
},
{
$set: {
type: KbTypeEnum.dataset,
parentId: null
}
}
);
const response = await PgClient.update(PgDatasetTableName, {
where: [['file_id', 'undefined']],
values: [{ key: 'file_id', value: '' }]
});
jsonRes(res, {
data: response.rowCount
});
} catch (error) {
jsonRes(res, {
code: 500,
error
});
}
}
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { authUser } from '@/service/utils/auth';
import { connectToDatabase } from '@/service/mongo';
import mongoose from 'mongoose';
import { PgClient } from '@/service/pg';
import { PgDatasetTableName } from '@/constants/plugin';
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try {
await connectToDatabase();
await authUser({ req, authRoot: true });
const data = await mongoose.connection.db
.collection('dataset.files')
.updateMany({}, { $set: { 'metadata.datasetUsed': true } });
// update pg data
const pg = await PgClient.query(`UPDATE ${PgDatasetTableName}
SET file_id = ''
WHERE (file_id = 'undefined' OR LENGTH(file_id) < 20) AND file_id != '';`);
jsonRes(res, {
data: {
data,
pg
}
});
} catch (error) {
jsonRes(res, {
code: 500,
error
});
}
}
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { authUser } from '@/service/utils/auth';
import { connectToDatabase, Bill } from '@/service/mongo';
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try {
await connectToDatabase();
await authUser({ req, authRoot: true });
try {
await Bill.collection.dropIndex('time_1');
} catch (error) {}
try {
await Bill.collection.createIndex({ time: 1 }, { expireAfterSeconds: 90 * 24 * 60 * 60 });
} catch (error) {}
jsonRes(res, {
data: {}
});
} catch (error) {
jsonRes(res, {
code: 500,
error
});
}
}
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { connectToDatabase } from '@/service/mongo';
import { authUser } from '@/service/utils/auth';
import { App } from '@/service/models/app';
import { AppModuleItemType } from '@/types/app';
export type Props = {
name: string;
avatar?: string;
modules: AppModuleItemType[];
};
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
try {
const { name, avatar, modules } = req.body as Props;
if (!name || !Array.isArray(modules)) {
throw new Error('缺少参数');
}
// 凭证校验
const { userId } = await authUser({ req, authToken: true });
await connectToDatabase();
// 上限校验
const authCount = await App.countDocuments({
userId
});
if (authCount >= 50) {
throw new Error('上限 50 个应用');
}
// 创建模型
const response = await App.create({
avatar,
name,
userId,
modules
});
jsonRes(res, {
data: response._id
});
} catch (err) {
jsonRes(res, {
code: 500,
error: err
});
}
}
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { connectToDatabase, Bill } from '@/service/mongo';
import { authUser } from '@/service/utils/auth';
import { Types } from 'mongoose';
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try {
const { appId, start, end } = req.body as { appId: string; start: number; end: number };
const { userId } = await authUser({ req, authToken: true });
await connectToDatabase();
const result = await Bill.aggregate([
{
$match: {
appId: new Types.ObjectId(appId),
userId: new Types.ObjectId(userId),
time: { $gte: new Date(start) }
}
},
{
$group: {
_id: {
year: { $year: '$time' },
month: { $month: '$time' },
day: { $dayOfMonth: '$time' }
},
total: { $sum: '$total' }
}
},
{
$project: {
_id: 0,
date: { $dateFromParts: { year: '$_id.year', month: '$_id.month', day: '$_id.day' } },
total: 1
}
},
{ $sort: { date: 1 } }
]);
jsonRes(res, {
data: result
});
} catch (err) {
jsonRes(res, {
code: 500,
error: err
});
}
}
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { Chat, App, connectToDatabase, Collection, OutLink } from '@/service/mongo';
import { authUser } from '@/service/utils/auth';
import { authApp } from '@/service/utils/auth';
/* 获取我的模型 */
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
try {
const { appId } = req.query as { appId: string };
if (!appId) {
throw new Error('参数错误');
}
// 凭证校验
const { userId } = await authUser({ req, authToken: true });
await connectToDatabase();
// 验证是否是该用户的 app
await authApp({
appId,
userId
});
// 删除对应的聊天
await Chat.deleteMany({
appId
});
// 删除收藏列表
await Collection.deleteMany({
modelId: appId
});
// 删除分享链接
await OutLink.deleteMany({
appId
});
// 删除模型
await App.deleteOne({
_id: appId,
userId
});
jsonRes(res);
} catch (err) {
jsonRes(res, {
code: 500,
error: err
});
}
}
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { connectToDatabase } from '@/service/mongo';
import { authUser } from '@/service/utils/auth';
import { authApp } from '@/service/utils/auth';
/* 获取我的模型 */
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
try {
const { appId } = req.query as { appId: string };
if (!appId) {
throw new Error('参数错误');
}
// 凭证校验
const { userId } = await authUser({ req, authToken: true });
await connectToDatabase();
const { app } = await authApp({
appId,
userId
});
jsonRes(res, {
data: app
});
} catch (err) {
jsonRes(res, {
code: 500,
error: err
});
}
}
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { Chat, connectToDatabase } from '@/service/mongo';
import { authUser } from '@/service/utils/auth';
import type { PagingData } from '@/types';
import { AppLogsListItemType } from '@/types/app';
import { Types } from 'mongoose';
import { addDays } from 'date-fns';
import { GetAppChatLogsParams } from '@/api/request/app';
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try {
const {
pageNum = 1,
pageSize = 20,
appId,
dateStart = addDays(new Date(), -7),
dateEnd = new Date()
} = req.body as GetAppChatLogsParams;
if (!appId) {
throw new Error('缺少参数');
}
await connectToDatabase();
// 凭证校验
const { userId } = await authUser({ req, authToken: true });
const where = {
appId: new Types.ObjectId(appId),
userId: new Types.ObjectId(userId),
updateTime: {
$gte: new Date(dateStart),
$lte: new Date(dateEnd)
}
};
const [data, total] = await Promise.all([
Chat.aggregate([
{ $match: where },
{
$lookup: {
from: 'chatitems',
let: { chat_id: '$chatId' },
pipeline: [
{
$match: {
$expr: {
$and: [
{ $eq: ['$chatId', '$$chat_id'] },
{ $eq: ['$appId', new Types.ObjectId(appId)] }
]
}
}
}
],
as: 'chatitems'
}
},
{
$addFields: {
feedbackCount: {
$size: {
$filter: {
input: '$chatitems',
as: 'item',
cond: { $ifNull: ['$$item.userFeedback', false] }
}
}
},
markCount: {
$size: {
$filter: {
input: '$chatitems',
as: 'item',
cond: { $ifNull: ['$$item.adminFeedback', false] }
}
}
}
}
},
{ $sort: { feedbackCount: -1, updateTime: -1 } },
{ $skip: (pageNum - 1) * pageSize },
{ $limit: pageSize },
{
$project: {
id: '$chatId',
title: 1,
source: 1,
time: '$updateTime',
messageCount: { $size: '$chatitems' },
feedbackCount: 1,
markCount: 1
}
}
]),
Chat.countDocuments(where)
]);
jsonRes<PagingData<AppLogsListItemType>>(res, {
data: {
pageNum,
pageSize,
data,
total
}
});
} catch (error) {
jsonRes(res, {
code: 500,
error
});
}
}
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
});
}
}
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { connectToDatabase, Collection, App } from '@/service/mongo';
import { authUser } from '@/service/utils/auth';
/* 模型收藏切换 */
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
try {
const { appId } = req.query as { appId: string };
if (!appId) {
throw new Error('缺少参数');
}
// 凭证校验
const { userId } = await authUser({ req, authToken: true });
await connectToDatabase();
const collectionRecord = await Collection.findOne({
userId,
modelId: appId
});
if (collectionRecord) {
await Collection.findByIdAndRemove(collectionRecord._id);
} else {
await Collection.create({
userId,
modelId: appId
});
}
await App.findByIdAndUpdate(appId, {
'share.collection': await Collection.countDocuments({ modelId: appId })
});
jsonRes(res);
} catch (err) {
jsonRes(res, {
code: 500,
error: err
});
}
}
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { connectToDatabase, App } from '@/service/mongo';
import type { PagingData } from '@/types';
import type { ShareAppItem } from '@/types/app';
import { authUser } from '@/service/utils/auth';
import { Types } from 'mongoose';
/* 获取模型列表 */
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
try {
const {
searchText = '',
pageNum = 1,
pageSize = 20
} = req.body as { searchText: string; pageNum: number; pageSize: number };
await connectToDatabase();
const { userId } = await authUser({ req, authToken: true });
const regex = new RegExp(searchText, 'i');
const where = {
$and: [
{ 'share.isShare': true },
{
$or: [{ name: { $regex: regex } }, { intro: { $regex: regex } }]
}
]
};
const pipeline = [
{
$match: where
},
{
$lookup: {
from: 'collections',
let: { modelId: '$_id' },
pipeline: [
{
$match: {
$expr: {
$and: [
{ $eq: ['$modelId', '$$modelId'] },
{
$eq: ['$userId', userId ? new Types.ObjectId(userId) : new Types.ObjectId()]
}
]
}
}
}
],
as: 'collections'
}
},
{
$project: {
_id: 1,
avatar: { $ifNull: ['$avatar', '/icon/logo.svg'] },
name: 1,
userId: 1,
intro: 1,
share: 1,
isCollection: {
$cond: {
if: { $gt: [{ $size: '$collections' }, 0] },
then: true,
else: false
}
}
}
},
{
$sort: { 'share.topNum': -1, 'share.collection': -1 }
},
{
$skip: (pageNum - 1) * pageSize
},
{
$limit: pageSize
}
];
// 获取被分享的模型
const [models, total] = await Promise.all([
// @ts-ignore
App.aggregate(pipeline),
App.countDocuments(where)
]);
jsonRes<PagingData<ShareAppItem>>(res, {
data: {
pageNum,
pageSize,
data: models,
total
}
});
} catch (err) {
jsonRes(res, {
code: 500,
error: err
});
}
}
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { connectToDatabase } from '@/service/mongo';
import { authUser } from '@/service/utils/auth';
import { App } from '@/service/models/app';
import type { AppUpdateParams } from '@/types/app';
import { authApp } from '@/service/utils/auth';
/* 获取我的模型 */
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
try {
const { name, avatar, type, chat, share, intro, modules } = req.body as AppUpdateParams;
const { appId } = req.query as { appId: string };
if (!appId) {
throw new Error('appId is empty');
}
// 凭证校验
const { userId } = await authUser({ req, authToken: true });
await connectToDatabase();
await authApp({
appId,
userId
});
// 更新模型
await App.updateOne(
{
_id: appId,
userId
},
{
name,
type,
avatar,
intro,
chat,
...(share && {
'share.isShare': share.isShare,
'share.isShareDetail': share.isShareDetail
}),
...(modules && { modules })
}
);
jsonRes(res);
} catch (err) {
jsonRes(res, {
code: 500,
error: err
});
}
}
import type { NextApiRequest, NextApiResponse } from 'next';
import { connectToDatabase } from '@/service/mongo';
import { authUser } from '@/service/utils/auth';
import { sseErrRes } from '@/service/response';
import { sseResponseEventEnum } from '@/constants/chat';
import { sseResponse } from '@/service/utils/tools';
import { AppModuleItemType } from '@/types/app';
import { dispatchModules } from '../openapi/v1/chat/completions';
import { pushChatBill } from '@/service/common/bill/push';
import { BillSourceEnum } from '@/constants/user';
import { ChatItemType } from '@/types/chat';
export type Props = {
history: ChatItemType[];
prompt: string;
modules: AppModuleItemType[];
variables: Record<string, any>;
appId: string;
appName: string;
};
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
res.on('close', () => {
res.end();
});
res.on('error', () => {
console.log('error: ', 'request error');
res.end();
});
let { modules = [], history = [], prompt, variables = {}, appName, appId } = req.body as Props;
try {
if (!history || !modules || !prompt) {
throw new Error('Prams Error');
}
if (!Array.isArray(modules)) {
throw new Error('history is not array');
}
await connectToDatabase();
/* user auth */
const { userId, user } = await authUser({ req, authBalance: true });
if (!user) {
throw new Error('user not found');
}
/* start process */
const { responseData } = await dispatchModules({
res,
modules: modules,
variables,
user,
params: {
history,
userChatInput: prompt
},
stream: true,
detail: true
});
sseResponse({
res,
event: sseResponseEventEnum.answer,
data: '[DONE]'
});
sseResponse({
res,
event: sseResponseEventEnum.appStreamResponse,
data: JSON.stringify(responseData)
});
res.end();
pushChatBill({
appName,
appId,
userId,
source: BillSourceEnum.fastgpt,
response: responseData
});
} catch (err: any) {
res.status(500);
sseErrRes(res, err);
res.end();
}
}
export const config = {
api: {
responseLimit: '20mb'
}
};
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
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 };
await connectToDatabase();
// 凭证校验
const { userId } = await authUser({ req, authToken: true });
// 删除一条数据库记录
await ChatItem.deleteOne({
dataId: contentId,
chatId,
userId
});
jsonRes(res);
} catch (err) {
jsonRes(res, {
code: 500,
error: err
});
}
}
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { connectToDatabase, ChatItem } from '@/service/mongo';
import { AdminUpdateFeedbackParams } from '@/api/request/chat';
import { authUser } from '@/service/utils/auth';
/* 初始化我的聊天框,需要身份验证 */
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try {
await connectToDatabase();
const { chatItemId, kbId, dataId, content = undefined } = req.body as AdminUpdateFeedbackParams;
if (!chatItemId || !kbId || !dataId || !content) {
throw new Error('missing parameter');
}
const { userId } = await authUser({ req, authToken: true });
await ChatItem.findOneAndUpdate(
{
userId,
dataId: chatItemId
},
{
adminFeedback: {
kbId,
dataId,
content
}
}
);
jsonRes(res);
} catch (err) {
jsonRes(res, {
code: 500,
error: err
});
}
}
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { connectToDatabase, ChatItem } from '@/service/mongo';
/* 初始化我的聊天框,需要身份验证 */
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try {
await connectToDatabase();
const { chatItemId, userFeedback = undefined } = req.body as {
chatItemId: string;
userFeedback?: string;
};
if (!chatItemId) {
throw new Error('chatItemId is required');
}
await ChatItem.findOneAndUpdate(
{
dataId: chatItemId
},
{
...(userFeedback ? { userFeedback } : { $unset: { userFeedback: '' } })
}
);
jsonRes(res);
} catch (err) {
jsonRes(res, {
code: 500,
error: err
});
}
}
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 { ChatHistoryItemType } from '@/types/chat';
import { ChatSourceEnum } from '@/constants/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,
source: ChatSourceEnum.online,
...(appId && { appId })
},
'chatId title top customTitle appId updateTime'
)
.sort({ top: -1, updateTime: -1 })
.limit(20);
jsonRes<ChatHistoryItemType[]>(res, {
data: data.map((item) => ({
chatId: item.chatId,
updateTime: item.updateTime,
appId: item.appId,
customTitle: item.customTitle,
title: item.title,
top: item.top
}))
});
} catch (err) {
jsonRes(res, {
code: 500,
error: err
});
}
}
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { connectToDatabase, Chat } from '@/service/mongo';
import { authUser } from '@/service/utils/auth';
export type Props = {
chatId: string;
customTitle?: string;
top?: boolean;
};
/* 更新聊天标题 */
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try {
const { chatId, customTitle, top } = req.body as Props;
const { userId } = await authUser({ req, authToken: true });
await connectToDatabase();
await Chat.findOneAndUpdate(
{
chatId,
userId
},
{
...(customTitle ? { customTitle } : {}),
...(top ? { top } : { top: null })
}
);
jsonRes(res);
} catch (err) {
jsonRes(res, {
code: 500,
error: err
});
}
}
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
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 type { ChatSchema } from '@/types/mongoSchema';
import { getSpecialModule, getChatModelNameList } from '@/components/ChatBox/utils';
import { TaskResponseKeyEnum } from '@/constants/chat';
/* 初始化我的聊天框,需要身份验证 */
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try {
const { userId } = await authUser({ req, authToken: true });
let { appId, chatId } = req.query as {
appId: '' | string;
chatId: '' | string;
};
if (!appId) {
return jsonRes(res, {
code: 501,
message: "You don't have an app yet"
});
}
// 校验使用权限
const app = (
await authApp({
appId,
userId,
authUser: false,
authOwner: false
})
).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,
appId
},
'title variables'
),
ChatItem.find(
{
chatId,
userId,
appId
},
`dataId obj value adminFeedback userFeedback ${TaskResponseKeyEnum.responseData}`
)
.sort({ _id: -1 })
.limit(30)
]);
if (!chat) {
throw new Error('聊天框不存在');
}
history.reverse();
return { app, history, chat };
}
return {};
})();
if (!app) {
throw new Error('Auth App Error');
}
const isOwner = String(app.userId) === userId;
jsonRes<InitChatResponse>(res, {
data: {
chatId,
appId,
app: {
...getSpecialModule(app.modules),
chatModels: getChatModelNameList(app.modules),
name: app.name,
avatar: app.avatar,
intro: app.intro,
canUse: app.share.isShare || isOwner
},
title: chat?.title || '新对话',
variables: chat?.variables || {},
history
}
});
} catch (err) {
jsonRes(res, {
code: 500,
error: err
});
}
}
export const config = {
api: {
responseLimit: '10mb'
}
};
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { connectToDatabase, Chat, ChatItem } from '@/service/mongo';
import { authUser } from '@/service/utils/auth';
import { ChatSourceEnum } from '@/constants/chat';
type Props = {
chatId?: string;
appId?: string;
};
/* clear chat history */
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try {
const { chatId, appId } = req.query as Props;
const { userId } = await authUser({ req, authToken: true });
await connectToDatabase();
if (chatId) {
await Promise.all([
Chat.findOneAndRemove({
chatId,
userId
}),
ChatItem.deleteMany({
userId,
chatId
})
]);
}
if (appId) {
const chats = await Chat.find({
appId,
userId,
source: ChatSourceEnum.online
}).select('_id');
const chatIds = chats.map((chat) => chat._id);
await Promise.all([
Chat.deleteMany({
_id: { $in: chatIds }
}),
ChatItem.deleteMany({
chatId: { $in: chatIds }
})
]);
}
jsonRes(res);
} catch (err) {
jsonRes(res, {
code: 500,
error: err
});
}
}
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { connectToDatabase, Bill } from '@/service/mongo';
import { authUser } from '@/service/utils/auth';
import { BillSourceEnum } from '@/constants/user';
import { CreateTrainingBillType } from '@/api/common/bill/index.d';
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try {
const { name } = req.body as CreateTrainingBillType;
const { userId } = await authUser({ req, authToken: true });
await connectToDatabase();
const { _id } = await Bill.create({
userId,
appName: name,
source: BillSourceEnum.training,
list: [
{
moduleName: '索引生成',
model: 'embedding',
amount: 0,
tokenLen: 0
},
{
moduleName: 'QA 拆分',
model: global.qaModel.name,
amount: 0,
tokenLen: 0
}
],
total: 0
});
jsonRes<string>(res, {
data: _id
});
} catch (err) {
jsonRes(res, {
code: 500,
error: err
});
}
}
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { connectToDatabase, KB } from '@/service/mongo';
import { authUser } from '@/service/utils/auth';
import { getVectorModel } from '@/service/utils/data';
import type { DatasetsItemType } from '@/types/core/dataset';
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
try {
// 凭证校验
const { userId } = await authUser({ req, authToken: true });
await connectToDatabase();
const kbList = await KB.find({
userId,
type: 'dataset'
});
const data = kbList.map((item) => ({
...item.toJSON(),
vectorModel: getVectorModel(item.vectorModel)
}));
jsonRes<DatasetsItemType[]>(res, {
data
});
} catch (err) {
jsonRes(res, {
code: 500,
error: err
});
}
}
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { connectToDatabase, KB } from '@/service/mongo';
import { authUser } from '@/service/utils/auth';
import type { CreateDatasetParams } from '@/api/core/dataset/index.d';
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
try {
const { name, tags, avatar, vectorModel, parentId, type } = req.body as CreateDatasetParams;
// 凭证校验
const { userId } = await authUser({ req, authToken: true });
await connectToDatabase();
const { _id } = await KB.create({
name,
userId,
tags,
vectorModel,
avatar,
parentId: parentId || null,
type
});
jsonRes(res, { data: _id });
} catch (err) {
jsonRes(res, {
code: 500,
error: err
});
}
}
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { authUser } from '@/service/utils/auth';
import { PgClient } from '@/service/pg';
import { withNextCors } from '@/service/utils/tools';
import { PgDatasetTableName } from '@/constants/plugin';
export default withNextCors(async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
try {
let { dataId } = req.query as {
dataId: string;
};
if (!dataId) {
throw new Error('缺少参数');
}
// 凭证校验
const { userId } = await authUser({ req });
await PgClient.delete(PgDatasetTableName, {
where: [['user_id', userId], 'AND', ['id', dataId]]
});
jsonRes(res);
} catch (err) {
console.log(err);
jsonRes(res, {
code: 500,
error: err
});
}
});
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { connectToDatabase, User } from '@/service/mongo';
import { authUser } from '@/service/utils/auth';
import { PgDatasetTableName } from '@/constants/plugin';
import { findAllChildrenIds } from '../delete';
import QueryStream from 'pg-query-stream';
import { PgClient } from '@/service/pg';
import { addLog } from '@/service/utils/tools';
import { responseWriteController } from '@/service/common/stream';
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
try {
let { kbId } = req.query as {
kbId: string;
};
if (!kbId || !global.pgClient) {
throw new Error('缺少参数');
}
await connectToDatabase();
// 凭证校验
const { userId } = await authUser({ req, authToken: true });
const exportIds = [kbId, ...(await findAllChildrenIds(kbId))];
const limitMinutesAgo = new Date(
Date.now() - (global.feConfigs?.limit?.exportLimitMinutes || 0) * 60 * 1000
);
// auth export times
const authTimes = await User.findOne(
{
_id: userId,
$or: [
{ 'limit.exportKbTime': { $exists: false } },
{ 'limit.exportKbTime': { $lte: limitMinutesAgo } }
]
},
'_id limit'
);
if (!authTimes) {
const minutes = `${global.feConfigs?.limit?.exportLimitMinutes || 0} 分钟`;
throw new Error(`上次导出未到 ${minutes},每 ${minutes}仅可导出一次。`);
}
const { rows } = await PgClient.query(
`SELECT count(id) FROM ${PgDatasetTableName} where user_id='${userId}' AND kb_id IN (${exportIds
.map((id) => `'${id}'`)
.join(',')})`
);
const total = rows?.[0]?.count || 0;
addLog.info(`export datasets: ${userId}`, { total });
if (total > 100000) {
throw new Error('数据量超出 10 万,无法导出');
}
// connect pg
global.pgClient.connect((err, client, done) => {
if (err) {
console.error(err);
res.end('Error connecting to database');
return;
}
console.log('export data');
// create pg select stream
const query = new QueryStream(
`SELECT q, a, source FROM ${PgDatasetTableName} where user_id='${userId}' AND kb_id IN (${exportIds
.map((id) => `'${id}'`)
.join(',')})`
);
const stream = client.query(query);
res.setHeader('Content-Disposition', 'attachment; filename=dataset.csv');
res.setHeader('Content-Type', 'text/csv');
res.write('index,content,source');
const write = responseWriteController({
res,
readStream: stream
});
// parse data every row
stream.on('data', ({ q, a, source }: { q: string; a: string; source?: string }) => {
if (res.closed) {
return stream.destroy();
}
write(`\n"${q}","${a || ''}","${source || ''}"`);
});
// finish
stream.on('end', async () => {
try {
// update export time
await User.findByIdAndUpdate(userId, {
'limit.exportKbTime': new Date()
});
} catch (error) {}
// close response
done();
res.end();
});
stream.on('error', (err) => {
done(err);
res.end('Error exporting data');
});
});
} catch (err) {
res.status(500);
jsonRes(res, {
code: 500,
error: err
});
}
}
export const config = {
api: {
responseLimit: '100mb'
}
};
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { connectToDatabase } from '@/service/mongo';
import { authUser } from '@/service/utils/auth';
import { PgClient } from '@/service/pg';
import { PgDatasetTableName } from '@/constants/plugin';
import type { PgDataItemType } from '@/types/core/dataset/data';
export type Response = {
id: string;
q: string;
a: string;
source: string;
};
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
try {
let { dataId } = req.query as {
dataId: string;
};
if (!dataId) {
throw new Error('缺少参数');
}
// 凭证校验
const { userId } = await authUser({ req, authToken: true });
await connectToDatabase();
const where: any = [['user_id', userId], 'AND', ['id', dataId]];
const searchRes = await PgClient.select<PgDataItemType>(PgDatasetTableName, {
fields: ['kb_id', 'id', 'q', 'a', 'source', 'file_id'],
where,
limit: 1
});
jsonRes(res, {
data: searchRes.rows[0]
});
} catch (err) {
jsonRes(res, {
code: 500,
error: err
});
}
}
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { connectToDatabase } from '@/service/mongo';
import { authUser } from '@/service/utils/auth';
import { PgClient } from '@/service/pg';
import { PgDatasetTableName } from '@/constants/plugin';
import { OtherFileId } from '@/constants/dataset';
import type { PgDataItemType } from '@/types/core/dataset/data';
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
try {
let {
kbId,
pageNum = 1,
pageSize = 10,
searchText = '',
fileId = ''
} = req.body as {
kbId: string;
pageNum: number;
pageSize: number;
searchText: string;
fileId: string;
};
if (!kbId) {
throw new Error('缺少参数');
}
// 凭证校验
const { userId } = await authUser({ req, authToken: true });
await connectToDatabase();
searchText = searchText.replace(/'/g, '');
const where: any = [
['user_id', userId],
'AND',
['kb_id', kbId],
...(fileId
? fileId === OtherFileId
? ["AND (file_id IS NULL OR file_id = '')"]
: ['AND', ['file_id', fileId]]
: []),
...(searchText
? [
'AND',
`(q LIKE '%${searchText}%' OR a LIKE '%${searchText}%' OR source LIKE '%${searchText}%')`
]
: [])
];
const [searchRes, total] = await Promise.all([
PgClient.select<PgDataItemType>(PgDatasetTableName, {
fields: ['id', 'q', 'a', 'source', 'file_id'],
where,
order: [{ field: 'id', mode: 'DESC' }],
limit: pageSize,
offset: pageSize * (pageNum - 1)
}),
PgClient.count(PgDatasetTableName, {
fields: ['id'],
where
})
]);
jsonRes(res, {
data: {
pageNum,
pageSize,
data: searchRes.rows,
total
}
});
} catch (err) {
jsonRes(res, {
code: 500,
error: err
});
}
}
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { TrainingData } from '@/service/mongo';
import { authUser } from '@/service/utils/auth';
/* 拆分数据成QA */
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try {
await authUser({ req, authToken: true });
// split queue data
const result = await TrainingData.countDocuments({
lockTime: { $lt: new Date('2040/1/1') }
});
jsonRes(res, {
data: result
});
} catch (err) {
jsonRes(res, {
code: 500,
error: err
});
}
}
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { connectToDatabase, TrainingData } from '@/service/mongo';
import { authUser } from '@/service/utils/auth';
import { TrainingModeEnum } from '@/constants/plugin';
import { Types } from 'mongoose';
import { startQueue } from '@/service/utils/tools';
/* 拆分数据成QA */
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try {
const { kbId, init = false } = req.body as { kbId: string; init: boolean };
if (!kbId) {
throw new Error('参数错误');
}
await connectToDatabase();
const { userId } = await authUser({ req, authToken: true });
// split queue data
const result = await TrainingData.aggregate([
{
$match: {
userId: new Types.ObjectId(userId),
kbId: new Types.ObjectId(kbId)
}
},
{
$group: {
_id: '$mode',
count: { $sum: 1 }
}
}
]);
jsonRes(res, {
data: {
qaListLen: result.find((item) => item._id === TrainingModeEnum.qa)?.count || 0,
vectorListLen: result.find((item) => item._id === TrainingModeEnum.index)?.count || 0
}
});
if (init) {
startQueue();
}
} catch (err) {
jsonRes(res, {
code: 500,
error: err
});
}
}
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { connectToDatabase } from '@/service/mongo';
import { authKb, authUser } from '@/service/utils/auth';
import { withNextCors } from '@/service/utils/tools';
import { PgDatasetTableName } from '@/constants/plugin';
import { insertData2Dataset, PgClient } from '@/service/pg';
import { getVectorModel } from '@/service/utils/data';
import { getVector } from '@/pages/api/openapi/plugin/vector';
import { DatasetDataItemType } from '@/types/core/dataset/data';
import { countPromptTokens } from '@/utils/common/tiktoken';
export type Props = {
billId?: string;
kbId: string;
data: DatasetDataItemType;
};
export default withNextCors(async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
try {
await connectToDatabase();
// 凭证校验
const { userId } = await authUser({ req });
jsonRes(res, {
data: await getVectorAndInsertDataset({
...req.body,
userId
})
});
} catch (err) {
jsonRes(res, {
code: 500,
error: err
});
}
});
export async function getVectorAndInsertDataset(
props: Props & { userId: string }
): Promise<string> {
const { kbId, data, userId, billId } = props;
if (!kbId || !data?.q) {
return Promise.reject('缺少参数');
}
// auth kb
const kb = await authKb({ kbId, userId });
const q = data?.q?.replace(/\\n/g, '\n').trim().replace(/'/g, '"');
const a = data?.a?.replace(/\\n/g, '\n').trim().replace(/'/g, '"');
// token check
const token = countPromptTokens(q, 'system');
if (token > getVectorModel(kb.vectorModel).maxToken) {
return Promise.reject('Over Tokens');
}
const { rows: existsRows } = await PgClient.query(`
SELECT COUNT(*) > 0 AS exists
FROM ${PgDatasetTableName}
WHERE md5(q)=md5('${q}') AND md5(a)=md5('${a}') AND user_id='${userId}' AND kb_id='${kbId}'
`);
const exists = existsRows[0]?.exists || false;
if (exists) {
return Promise.reject('已经存在完全一致的数据');
}
const { vectors } = await getVector({
model: kb.vectorModel,
input: [q],
userId,
billId
});
const response = await insertData2Dataset({
userId,
kbId,
data: [
{
...data,
q,
a,
vector: vectors[0]
}
]
});
// @ts-ignore
return response?.rows?.[0]?.id || '';
}
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { connectToDatabase, TrainingData, KB } from '@/service/mongo';
import { authUser } from '@/service/utils/auth';
import { authKb } from '@/service/utils/auth';
import { withNextCors } from '@/service/utils/tools';
import { PgDatasetTableName, TrainingModeEnum } from '@/constants/plugin';
import { startQueue } from '@/service/utils/tools';
import { PgClient } from '@/service/pg';
import { getVectorModel } from '@/service/utils/data';
import { DatasetDataItemType } from '@/types/core/dataset/data';
import { countPromptTokens } from '@/utils/common/tiktoken';
import type { PushDataProps, PushDataResponse } from '@/api/core/dataset/data.d';
const modeMap = {
[TrainingModeEnum.index]: true,
[TrainingModeEnum.qa]: true
};
export default withNextCors(async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
try {
const { kbId, data, mode = TrainingModeEnum.index } = req.body as PushDataProps;
if (!kbId || !Array.isArray(data)) {
throw new Error('KbId or data is empty');
}
if (modeMap[mode] === undefined) {
throw new Error('Mode is error');
}
if (data.length > 500) {
throw new Error('Data is too long, max 500');
}
await connectToDatabase();
// 凭证校验
const { userId } = await authUser({ req });
jsonRes<PushDataResponse>(res, {
data: await pushDataToKb({
...req.body,
userId
})
});
} catch (err) {
jsonRes(res, {
code: 500,
error: err
});
}
});
export async function pushDataToKb({
userId,
kbId,
data,
mode,
prompt,
billId
}: { userId: string } & PushDataProps): Promise<PushDataResponse> {
const [kb, vectorModel] = await Promise.all([
authKb({
userId,
kbId
}),
(async () => {
if (mode === TrainingModeEnum.index) {
const vectorModel = (await KB.findById(kbId, 'vectorModel'))?.vectorModel;
return getVectorModel(vectorModel || global.vectorModels[0].model);
}
return global.vectorModels[0];
})()
]);
const modeMaxToken = {
[TrainingModeEnum.index]: vectorModel.maxToken * 1.5,
[TrainingModeEnum.qa]: global.qaModel.maxToken * 0.8
};
// 过滤重复的 qa 内容
const set = new Set();
const filterData: DatasetDataItemType[] = [];
data.forEach((item) => {
if (!item.q) return;
const text = item.q + item.a;
// count q token
const token = countPromptTokens(item.q, 'system');
if (token > modeMaxToken[mode]) {
return;
}
if (!set.has(text)) {
filterData.push(item);
set.add(text);
}
});
// 数据库去重
const insertData = (
await Promise.allSettled(
filterData.map(async (data) => {
let { q, a } = data;
if (mode !== TrainingModeEnum.index) {
return Promise.resolve(data);
}
if (!q) {
return Promise.reject('q为空');
}
q = q.replace(/\\n/g, '\n').trim().replace(/'/g, '"');
a = a.replace(/\\n/g, '\n').trim().replace(/'/g, '"');
// Exactly the same data, not push
try {
const { rows } = await PgClient.query(`
SELECT COUNT(*) > 0 AS exists
FROM ${PgDatasetTableName}
WHERE md5(q)=md5('${q}') AND md5(a)=md5('${a}') AND user_id='${userId}' AND kb_id='${kbId}'
`);
const exists = rows[0]?.exists || false;
if (exists) {
return Promise.reject('已经存在');
}
} catch (error) {
console.log(error);
}
return Promise.resolve(data);
})
)
)
.filter((item) => item.status === 'fulfilled')
.map<DatasetDataItemType>((item: any) => item.value);
// 插入记录
const insertRes = await TrainingData.insertMany(
insertData.map((item) => ({
...item,
userId,
kbId,
mode,
prompt,
billId,
vectorModel: vectorModel.model
}))
);
insertRes.length > 0 && startQueue();
return {
insertLen: insertRes.length
};
}
export const config = {
api: {
bodyParser: {
sizeLimit: '10mb'
},
responseLimit: '12mb'
}
};
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { authUser } from '@/service/utils/auth';
import { PgClient } from '@/service/pg';
import { withNextCors } from '@/service/utils/tools';
import { KB, connectToDatabase } from '@/service/mongo';
import { getVector } from '@/pages/api/openapi/plugin/vector';
import { PgDatasetTableName } from '@/constants/plugin';
import type { UpdateDataPrams } from '@/api/core/dataset/data.d';
export default withNextCors(async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
try {
const { dataId, a = '', q = '', kbId } = req.body as UpdateDataPrams;
if (!dataId) {
throw new Error('缺少参数');
}
await connectToDatabase();
// auth user and get kb
const [{ userId }, kb] = await Promise.all([
authUser({ req }),
KB.findById(kbId, 'vectorModel')
]);
if (!kb) {
throw new Error("Can't find database");
}
// get vector
const { vectors = [] } = await (async () => {
if (q) {
return getVector({
userId,
input: [q],
model: kb.vectorModel
});
}
return { vectors: [[]] };
})();
// 更新 pg 内容.仅修改a,不需要更新向量。
await PgClient.update(PgDatasetTableName, {
where: [['id', dataId], 'AND', ['user_id', userId]],
values: [
{ key: 'a', value: a.replace(/'/g, '"') },
...(q
? [
{ key: 'q', value: q.replace(/'/g, '"') },
{ key: 'vector', value: `[${vectors[0]}]` }
]
: [])
]
});
jsonRes(res);
} catch (err) {
jsonRes(res, {
code: 500,
error: err
});
}
});
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { connectToDatabase, KB, TrainingData } from '@/service/mongo';
import { authUser } from '@/service/utils/auth';
import { PgClient } from '@/service/pg';
import { PgDatasetTableName } from '@/constants/plugin';
import { GridFSStorage } from '@/service/lib/gridfs';
import { Types } from 'mongoose';
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
try {
await connectToDatabase();
const { id } = req.query as {
id: string;
};
if (!id) {
throw new Error('缺少参数');
}
// 凭证校验
const { userId } = await authUser({ req, authToken: true });
const deletedIds = [id, ...(await findAllChildrenIds(id))];
// delete training data
await TrainingData.deleteMany({
userId,
kbId: { $in: deletedIds.map((id) => new Types.ObjectId(id)) }
});
// delete all pg data
await PgClient.delete(PgDatasetTableName, {
where: [
['user_id', userId],
'AND',
`kb_id IN (${deletedIds.map((id) => `'${id}'`).join(',')})`
]
});
// delete related files
const gridFs = new GridFSStorage('dataset', userId);
await Promise.all(deletedIds.map((id) => gridFs.deleteFilesByKbId(id)));
// delete kb data
await KB.deleteMany({
_id: { $in: deletedIds },
userId
});
jsonRes(res);
} catch (err) {
jsonRes(res, {
code: 500,
error: err
});
}
}
export async function findAllChildrenIds(id: string) {
// find children
const children = await KB.find({ parentId: id });
let allChildrenIds = children.map((child) => String(child._id));
for (const child of children) {
const grandChildrenIds = await findAllChildrenIds(child._id);
allChildrenIds = allChildrenIds.concat(grandChildrenIds);
}
return allChildrenIds;
}
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { connectToDatabase, KB } from '@/service/mongo';
import { authUser } from '@/service/utils/auth';
import { getVectorModel } from '@/service/utils/data';
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
try {
const { id } = req.query as {
id: string;
};
if (!id) {
throw new Error('缺少参数');
}
// 凭证校验
const { userId } = await authUser({ req, authToken: true });
await connectToDatabase();
const data = await KB.findOne({
_id: id,
userId
});
if (!data) {
throw new Error('kb is not exist');
}
jsonRes(res, {
data: {
_id: data._id,
avatar: data.avatar,
name: data.name,
userId: data.userId,
vectorModel: getVectorModel(data.vectorModel),
tags: data.tags.join(' ')
}
});
} catch (err) {
jsonRes(res, {
code: 500,
error: err
});
}
}
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { connectToDatabase, TrainingData } from '@/service/mongo';
import { authUser } from '@/service/utils/auth';
import { GridFSStorage } from '@/service/lib/gridfs';
import { PgClient } from '@/service/pg';
import { PgDatasetTableName } from '@/constants/plugin';
import { Types } from 'mongoose';
import { OtherFileId } from '@/constants/dataset';
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
try {
await connectToDatabase();
const { fileId, kbId } = req.query as { fileId: string; kbId: string };
if (!fileId || !kbId) {
throw new Error('fileId and kbId is required');
}
// 凭证校验
const { userId } = await authUser({ req, authToken: true });
// other data. Delete only vector data
if (fileId === OtherFileId) {
await PgClient.delete(PgDatasetTableName, {
where: [
['user_id', userId],
'AND',
['kb_id', kbId],
"AND (file_id IS NULL OR file_id = '')"
]
});
} else {
// auth file
const gridFs = new GridFSStorage('dataset', userId);
const bucket = gridFs.GridFSBucket();
await gridFs.findAndAuthFile(fileId);
// delete all pg data
await PgClient.delete(PgDatasetTableName, {
where: [['user_id', userId], 'AND', ['kb_id', kbId], 'AND', ['file_id', fileId]]
});
// delete all training data
await TrainingData.deleteMany({
userId,
file_id: fileId
});
// delete file
await bucket.delete(new Types.ObjectId(fileId));
}
jsonRes(res);
} catch (err) {
jsonRes(res, {
code: 500,
error: err
});
}
}
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { connectToDatabase } from '@/service/mongo';
import { authUser } from '@/service/utils/auth';
import { GridFSStorage } from '@/service/lib/gridfs';
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
try {
await connectToDatabase();
const { kbId } = req.query as { kbId: string };
// 凭证校验
const { userId } = await authUser({ req, authToken: true });
const gridFs = new GridFSStorage('dataset', userId);
const collection = gridFs.Collection();
const files = await collection.deleteMany({
uploadDate: { $lte: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000) },
['metadata.kbId']: kbId,
['metadata.userId']: userId,
['metadata.datasetUsed']: { $ne: true }
});
jsonRes(res, {
data: files
});
} catch (err) {
jsonRes(res);
}
}
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { connectToDatabase } from '@/service/mongo';
import { authUser } from '@/service/utils/auth';
import { GridFSStorage } from '@/service/lib/gridfs';
import { OtherFileId } from '@/constants/dataset';
import type { GSFileInfoType } from '@/types/common/file';
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
try {
await connectToDatabase();
const { fileId } = req.query as { kbId: string; fileId: string };
// 凭证校验
const { userId } = await authUser({ req, authToken: true });
if (fileId === OtherFileId) {
return jsonRes<GSFileInfoType>(res, {
data: {
id: OtherFileId,
size: 0,
filename: 'kb.Other Data',
uploadDate: new Date(),
encoding: '',
contentType: ''
}
});
}
const gridFs = new GridFSStorage('dataset', userId);
const file = await gridFs.findAndAuthFile(fileId);
jsonRes<GSFileInfoType>(res, {
data: file
});
} catch (err) {
jsonRes(res, {
code: 500,
error: err
});
}
}
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { connectToDatabase, TrainingData } from '@/service/mongo';
import { authUser } from '@/service/utils/auth';
import { GridFSStorage } from '@/service/lib/gridfs';
import { PgClient } from '@/service/pg';
import { PgDatasetTableName } from '@/constants/plugin';
import { FileStatusEnum, OtherFileId } from '@/constants/dataset';
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
try {
await connectToDatabase();
let {
pageNum = 1,
pageSize = 10,
kbId,
searchText = ''
} = req.body as { pageNum: number; pageSize: number; kbId: string; searchText: string };
searchText = searchText?.replace(/'/g, '');
// 凭证校验
const { userId } = await authUser({ req, authToken: true });
// find files
const gridFs = new GridFSStorage('dataset', userId);
const collection = gridFs.Collection();
const mongoWhere = {
['metadata.kbId']: kbId,
['metadata.userId']: userId,
['metadata.datasetUsed']: true,
...(searchText && { filename: { $regex: searchText } })
};
const [files, total] = await Promise.all([
collection
.find(mongoWhere, {
projection: {
_id: 1,
filename: 1,
uploadDate: 1,
length: 1
}
})
.skip((pageNum - 1) * pageSize)
.limit(pageSize)
.sort({ uploadDate: -1 })
.toArray(),
collection.countDocuments(mongoWhere)
]);
async function GetOtherData() {
return {
id: OtherFileId,
size: 0,
filename: 'kb.Other Data',
uploadTime: new Date(),
status: (await TrainingData.findOne({ userId, kbId, file_id: '' }))
? FileStatusEnum.embedding
: FileStatusEnum.ready,
chunkLength: await PgClient.count(PgDatasetTableName, {
fields: ['id'],
where: [
['user_id', userId],
'AND',
['kb_id', kbId],
"AND (file_id IS NULL OR file_id = '')"
]
})
};
}
const data = await Promise.all([
GetOtherData(),
...files.map(async (file) => {
return {
id: String(file._id),
size: file.length,
filename: file.filename,
uploadTime: file.uploadDate,
status: (await TrainingData.findOne({ userId, kbId, file_id: file._id }))
? FileStatusEnum.embedding
: FileStatusEnum.ready,
chunkLength: await PgClient.count(PgDatasetTableName, {
fields: ['id'],
where: [
['user_id', userId],
'AND',
['kb_id', kbId],
'AND',
['file_id', String(file._id)]
]
})
};
})
]);
jsonRes(res, {
data: {
pageNum,
pageSize,
data: data.flat(),
total
}
});
} catch (err) {
jsonRes(res, {
code: 500,
error: err
});
}
}
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { connectToDatabase } from '@/service/mongo';
import { authUser } from '@/service/utils/auth';
import { GridFSStorage } from '@/service/lib/gridfs';
import { MarkFileUsedProps } from '@/api/core/dataset/file.d';
import { Types } from 'mongoose';
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
try {
await connectToDatabase();
const { fileIds } = req.body as MarkFileUsedProps;
const { userId } = await authUser({ req, authToken: true });
const gridFs = new GridFSStorage('dataset', userId);
const collection = gridFs.Collection();
await collection.updateMany(
{
_id: { $in: fileIds.map((id) => new Types.ObjectId(id)) },
['metadata.userId']: userId
},
{
$set: {
['metadata.datasetUsed']: true
}
}
);
jsonRes(res, {});
} catch (err) {
jsonRes(res, {
code: 500,
error: err
});
}
}
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { connectToDatabase } from '@/service/mongo';
import { authUser } from '@/service/utils/auth';
import { GridFSStorage } from '@/service/lib/gridfs';
import { UpdateFileProps } from '@/api/core/dataset/file.d';
import { Types } from 'mongoose';
import { PgClient } from '@/service/pg';
import { PgDatasetTableName } from '@/constants/plugin';
import { addLog } from '@/service/utils/tools';
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
try {
await connectToDatabase();
const { id, name, datasetUsed } = req.body as UpdateFileProps;
const { userId } = await authUser({ req, authToken: true });
const gridFs = new GridFSStorage('dataset', userId);
const collection = gridFs.Collection();
await collection.findOneAndUpdate(
{
_id: new Types.ObjectId(id)
},
{
$set: {
...(name && { filename: name }),
...(datasetUsed && { ['metadata.datasetUsed']: datasetUsed })
}
}
);
// data source
updateDatasetSource({
fileId: id,
userId,
name
});
jsonRes(res, {});
} catch (err) {
jsonRes(res, {
code: 500,
error: err
});
}
}
async function updateDatasetSource(data: { fileId: string; userId: string; name?: string }) {
const { fileId, userId, name } = data;
if (!fileId || !name || !userId) return;
try {
await PgClient.update(PgDatasetTableName, {
where: [['user_id', userId], 'AND', ['file_id', fileId]],
values: [
{
key: 'source',
value: name
}
]
});
} catch (error) {
addLog.error(`Update dataset source error`, error);
setTimeout(() => {
updateDatasetSource(data);
}, 2000);
}
}
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { connectToDatabase, KB } from '@/service/mongo';
import { authUser } from '@/service/utils/auth';
import { getVectorModel } from '@/service/utils/data';
import type { DatasetsItemType } from '@/types/core/dataset';
import { KbTypeEnum } from '@/constants/dataset';
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
try {
const { parentId, type } = req.query as { parentId?: string; type?: `${KbTypeEnum}` };
// 凭证校验
const { userId } = await authUser({ req, authToken: true });
await connectToDatabase();
const kbList = await KB.find({
userId,
...(parentId !== undefined && { parentId: parentId || null }),
...(type && { type })
}).sort({ updateTime: -1 });
const data = await Promise.all(
kbList.map(async (item) => ({
...item.toJSON(),
vectorModel: getVectorModel(item.vectorModel)
}))
);
jsonRes<DatasetsItemType[]>(res, {
data
});
} catch (err) {
jsonRes(res, {
code: 500,
error: err
});
}
}
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { connectToDatabase, KB } from '@/service/mongo';
import type { DatasetPathItemType } from '@/types/core/dataset';
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
try {
await connectToDatabase();
const { parentId } = req.query as { parentId: string };
jsonRes<DatasetPathItemType[]>(res, {
data: await getParents(parentId)
});
} catch (err) {
jsonRes(res, {
code: 500,
error: err
});
}
}
async function getParents(parentId?: string): Promise<DatasetPathItemType[]> {
if (!parentId) {
return [];
}
const parent = await KB.findById(parentId, 'name parentId');
if (!parent) return [];
const paths = await getParents(parent.parentId);
paths.push({ parentId, parentName: parent.name });
return paths;
}
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { authUser } from '@/service/utils/auth';
import { PgClient } from '@/service/pg';
import { withNextCors } from '@/service/utils/tools';
import { getVector } from '../../openapi/plugin/vector';
import { PgDatasetTableName } from '@/constants/plugin';
import { KB } from '@/service/mongo';
import type { SearchTestProps, SearchTestResponseType } from '@/api/core/dataset/index.d';
export default withNextCors(async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
try {
const { kbId, text } = req.body as SearchTestProps;
if (!kbId || !text) {
throw new Error('缺少参数');
}
// 凭证校验
const [{ userId }, kb] = await Promise.all([
authUser({ req }),
KB.findById(kbId, 'vectorModel')
]);
if (!userId || !kb) {
throw new Error('缺少用户ID');
}
const { vectors } = await getVector({
model: kb.vectorModel,
userId,
input: [text]
});
const response: any = await PgClient.query(
`BEGIN;
SET LOCAL ivfflat.probes = ${global.systemEnv.pgIvfflatProbe || 10};
select id, q, a, source, file_id, (vector <#> '[${
vectors[0]
}]') * -1 AS score from ${PgDatasetTableName} where kb_id='${kbId}' AND user_id='${userId}' order by vector <#> '[${
vectors[0]
}]' limit 12;
COMMIT;`
);
jsonRes<SearchTestResponseType>(res, {
data: response?.[2]?.rows || []
});
} catch (err) {
console.log(err);
jsonRes(res, {
code: 500,
error: err
});
}
});
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { connectToDatabase, KB } from '@/service/mongo';
import { authUser } from '@/service/utils/auth';
import type { DatasetUpdateParams } from '@/api/core/dataset/index.d';
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
try {
const { id, parentId, name, avatar, tags } = req.body as DatasetUpdateParams;
if (!id) {
throw new Error('缺少参数');
}
// 凭证校验
const { userId } = await authUser({ req, authToken: true });
await connectToDatabase();
await KB.findOneAndUpdate(
{
_id: id,
userId
},
{
...(parentId !== undefined && { parentId: parentId || null }),
...(name && { name }),
...(avatar && { avatar }),
...(typeof tags === 'string' && {
tags: tags.split(' ').filter((item) => item)
})
}
);
jsonRes(res);
} catch (err) {
jsonRes(res, {
code: 500,
error: err
});
}
}
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { authBalanceByUid, authUser } from '@/service/utils/auth';
import { withNextCors } from '@/service/utils/tools';
import { getAIChatApi, axiosConfig } from '@/service/lib/openai';
import { pushGenerateVectorBill } from '@/service/common/bill/push';
type Props = {
model: string;
input: string[];
billId?: string;
};
type Response = {
tokenLen: number;
vectors: number[][];
};
export default withNextCors(async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
try {
const { userId } = await authUser({ req });
let { input, model } = req.query as Props;
if (!Array.isArray(input)) {
throw new Error('缺少参数');
}
jsonRes<Response>(res, {
data: await getVector({ userId, input, model })
});
} catch (err) {
console.log(err);
jsonRes(res, {
code: 500,
error: err
});
}
});
export async function getVector({
model = 'text-embedding-ada-002',
userId,
input,
billId
}: { userId?: string } & Props) {
userId && (await authBalanceByUid(userId));
for (let i = 0; i < input.length; i++) {
if (!input[i]) {
return Promise.reject({
code: 500,
message: '向量生成模块输入内容为空'
});
}
}
// 获取 chatAPI
const chatAPI = getAIChatApi();
// 把输入的内容转成向量
const result = await chatAPI
.createEmbedding(
{
model,
input
},
{
timeout: 60000,
...axiosConfig()
}
)
.then(async (res) => {
if (!res.data?.data?.[0]?.embedding) {
console.log(res.data);
// @ts-ignore
return Promise.reject(res.data?.err?.message || 'Embedding API Error');
}
return {
tokenLen: res.data.usage.total_tokens || 0,
vectors: await Promise.all(res.data.data.map((item) => unityDimensional(item.embedding)))
};
});
userId &&
pushGenerateVectorBill({
userId,
tokenLen: result.tokenLen,
model,
billId
});
return result;
}
function unityDimensional(vector: number[]) {
if (vector.length > 1536) return Promise.reject('向量维度不能超过 1536');
let resultVector = vector;
const vectorLen = vector.length;
const zeroVector = new Array(1536 - vectorLen).fill(0);
return resultVector.concat(zeroVector);
}
// 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, ChatItem } from '@/service/mongo';
import { Types } from 'mongoose';
import type { ChatItemType } from '@/types/chat';
export type Props = {
appId?: string;
chatId?: string;
limit?: number;
};
export type Response = { history: ChatItemType[] };
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try {
await connectToDatabase();
const { userId } = await authUser({ req });
const { chatId, limit } = req.body as Props;
jsonRes<Response>(res, {
data: await getChatHistory({
chatId,
userId,
limit
})
});
} catch (err) {
jsonRes(res, {
code: 500,
error: err
});
}
}
export async function getChatHistory({
chatId,
userId,
appId,
limit = 30
}: Props & { userId: string }): Promise<Response> {
if (!chatId || !appId) {
return { history: [] };
}
const history = await ChatItem.aggregate([
{
$match: {
chatId,
appId: new Types.ObjectId(appId),
userId: new Types.ObjectId(userId)
}
},
{
$sort: {
_id: -1
}
},
{
$limit: limit
},
{
$project: {
dataId: 1,
obj: 1,
value: 1
}
}
]);
history.reverse();
return { history };
}
This source diff could not be displayed because it is too large. You can view the blob instead.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment