V4.8.16 dev (#3431)

* feat: add feishu & yuque dataset (#3379)

* feat: add feishu & yuque dataset

* fix ts

* fix ts

* move type position

* fix

* fix: merge interface

* fix

* feat: dingtalk sso support (#3408)

* fix: optional sso state

* feat: dingtalk bot

* feat: dingtalk sso login

* chore: move i18n to user namespace

* feat: dingtalk bot integration (#3415)

* feat: dingtalk bot integration

* docs: config dingtalk bot

* feat:sear XNG服务 (#3413)

* feat:sear XNG服务

* 补充了courseUrl

* 添加了官方文档

* 错误时返回情况修正了一下

* Tracks (#3420)

* feat: node intro

* feat: add domain track

* dingding sso login

* perf: api dataset code and add doc

* feat: tracks

* feat: searXNG plugins

* fix: ts

* feat: delete node tracks (#3423)

* fix: dingtalk bot GET verification (#3424)

* 4.8.16 test: fix: plugin inputs render;fix: ui offset (#3426)

* fix: ui offset

* perf: dingding talk

* fix: plugin inputs render

* feat: menu all folder (#3429)

* fix: recall code

---------

Co-authored-by: heheer <heheer@sealos.io>
Co-authored-by: a.e. <49438478+I-Info@users.noreply.github.com>
Co-authored-by: Jiangween <145003935+Jiangween@users.noreply.github.com>
This commit is contained in:
Archer
2024-12-18 19:30:19 +08:00
committed by GitHub
parent 82871be054
commit bd79e7701f
154 changed files with 2519 additions and 300 deletions

View File

@@ -75,7 +75,6 @@
"@faker-js/faker": "^9.0.3",
"@shelf/jest-mongodb": "^4.3.2",
"@svgr/webpack": "^6.5.1",
"@types/faker": "^6.6.9",
"@types/formidable": "^2.0.5",
"@types/js-yaml": "^4.0.9",
"@types/jsonwebtoken": "^9.0.3",

Binary file not shown.

After

Width:  |  Height:  |  Size: 143 KiB

View File

@@ -2,7 +2,7 @@ import {
PushDatasetDataChunkProps,
PushDatasetDataResponse
} from '@fastgpt/global/core/dataset/api';
import { APIFileServer } from '@fastgpt/global/core/dataset/apiDataset';
import { APIFileServer, FeishuServer, YuqueServer } from '@fastgpt/global/core/dataset/apiDataset';
import {
DatasetSearchModeEnum,
DatasetSourceReadTypeEnum,
@@ -27,6 +27,8 @@ export type CreateDatasetParams = {
vectorModel?: string;
agentModel?: string;
apiServer?: APIFileServer;
feishuServer?: FeishuServer;
yuqueServer?: YuqueServer;
};
export type RebuildEmbeddingProps = {

View File

@@ -29,7 +29,7 @@ async function handler(req: ApiRequestProps<PreviewContextProps>, res: NextApiRe
throw new Error('fileId is empty');
}
const { teamId, apiServer } = await (async () => {
const { teamId, apiServer, feishuServer, yuqueServer } = await (async () => {
if (type === DatasetSourceReadTypeEnum.fileLocal) {
const res = await authCollectionFile({
req,
@@ -51,7 +51,9 @@ async function handler(req: ApiRequestProps<PreviewContextProps>, res: NextApiRe
});
return {
teamId: dataset.teamId,
apiServer: dataset.apiServer
apiServer: dataset.apiServer,
feishuServer: dataset.feishuServer,
yuqueServer: dataset.yuqueServer
};
})();
@@ -62,6 +64,8 @@ async function handler(req: ApiRequestProps<PreviewContextProps>, res: NextApiRe
isQAImport,
selector,
apiServer,
feishuServer,
yuqueServer,
externalFileId
});

View File

@@ -0,0 +1,49 @@
import type { ApiRequestProps, ApiResponseType } from '@fastgpt/service/type/next';
import { NextAPI } from '@/service/middleware/entry';
import { addLog } from '@fastgpt/service/common/system/log';
import { TrackEnum } from '@fastgpt/global/common/middle/tracks/constants';
import { TrackModel } from '@fastgpt/service/common/middle/tracks/schema';
import { authCert } from '@fastgpt/service/support/permission/auth/common';
import { useReqFrequencyLimit } from '@fastgpt/service/common/middle/reqFrequencyLimit';
export type pushQuery = {};
export type pushBody = {
event: TrackEnum;
data: any;
};
export type pushResponse = {};
async function handler(
req: ApiRequestProps<pushBody, pushQuery>,
res: ApiResponseType<any>
): Promise<pushResponse> {
if (!global.feConfigs?.isPlus) return {};
const { teamId, tmbId, userId } = await authCert({
req,
authToken: true
});
const data = {
teamId,
tmbId,
uid: userId,
event: req.body.event,
data: req.body.data
};
addLog.info('Push tracks', data);
return TrackModel.create(data);
}
export default NextAPI(useReqFrequencyLimit(1, 5), handler);
export const config = {
api: {
bodyParser: {
sizeLimit: '5kb'
}
}
};

View File

@@ -16,6 +16,7 @@ import { authApp } from '@fastgpt/service/support/permission/app/auth';
import { CommonErrEnum } from '@fastgpt/global/common/error/code/common';
import { MongoTeamMember } from '@fastgpt/service/support/user/team/teamMemberSchema';
import { MongoUser } from '@fastgpt/service/support/user/schema';
import { pushTrack } from '@fastgpt/service/common/middle/tracks/utils';
export type CreateAppBody = {
parentId?: ParentIdType;
@@ -35,7 +36,7 @@ async function handler(req: ApiRequestProps<CreateAppBody>) {
}
// 凭证校验
const [{ teamId, tmbId }] = await Promise.all([
const [{ teamId, tmbId, userId }] = await Promise.all([
authUserPer({ req, authToken: true, per: WritePermissionVal }),
...(parentId
? [authApp({ req, appId: parentId, per: WritePermissionVal, authToken: true })]
@@ -62,6 +63,13 @@ async function handler(req: ApiRequestProps<CreateAppBody>) {
username: user?.username
});
pushTrack.createApp({
type,
uid: userId,
teamId,
tmbId
});
return appId;
}

View File

@@ -16,6 +16,8 @@ import { findAppAndAllChildren } from '@fastgpt/service/core/app/controller';
import { MongoResourcePermission } from '@fastgpt/service/support/permission/schema';
import { ClientSession } from '@fastgpt/service/common/mongo';
import { deleteChatFiles } from '@fastgpt/service/core/chat/controller';
import { getAppLatestVersion } from '@fastgpt/service/core/app/version/controller';
import { pushTrack } from '@fastgpt/service/common/middle/tracks/utils';
async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
const { appId } = req.query as { appId: string };
@@ -25,12 +27,20 @@ async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
}
// Auth owner (folder owner, can delete all apps in the folder)
const { teamId } = await authApp({ req, authToken: true, appId, per: OwnerPermissionVal });
const { teamId, tmbId, userId, app } = await authApp({
req,
authToken: true,
appId,
per: OwnerPermissionVal
});
await onDelOneApp({
teamId,
appId
});
// Tracks
pushTrack.countAppNodes({ teamId, tmbId, uid: userId, appId });
}
export default NextAPI(handler);

View File

@@ -8,6 +8,7 @@ import { NextAPI } from '@/service/middleware/entry';
import { onCreateApp, type CreateAppBody } from '../create';
import { AppSchema } from '@fastgpt/global/core/app/type';
import { AppTypeEnum } from '@fastgpt/global/core/app/constants';
import { pushTrack } from '@fastgpt/service/common/middle/tracks/utils';
export type createHttpPluginQuery = {};
@@ -28,7 +29,11 @@ async function handler(
return Promise.reject('缺少参数');
}
const { teamId, tmbId } = await authUserPer({ req, authToken: true, per: WritePermissionVal });
const { teamId, tmbId, userId } = await authUserPer({
req,
authToken: true,
per: WritePermissionVal
});
await mongoSessionRun(async (session) => {
// create http plugin folder
@@ -62,6 +67,13 @@ async function handler(
}
});
pushTrack.createApp({
type: AppTypeEnum.httpPlugin,
uid: userId,
teamId,
tmbId
});
return {};
}

View File

@@ -45,7 +45,9 @@ async function handler(
const { nodes, chatConfig } = await getAppLatestVersion(app._id, app);
const pluginInputs =
app?.modules?.find((node) => node.flowNodeType === FlowNodeTypeEnum.pluginInput)?.inputs ?? [];
chat?.pluginInputs ??
nodes?.find((node) => node.flowNodeType === FlowNodeTypeEnum.pluginInput)?.inputs ??
[];
return {
chatId,

View File

@@ -35,6 +35,10 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
}
const { nodes, chatConfig } = await getAppLatestVersion(app._id, app);
const pluginInputs =
chat?.pluginInputs ??
nodes?.find((node) => node.flowNodeType === FlowNodeTypeEnum.pluginInput)?.inputs ??
[];
jsonRes<InitChatResponse>(res, {
data: {
@@ -56,9 +60,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
avatar: app.avatar,
intro: app.intro,
type: app.type,
pluginInputs:
app?.modules?.find((node) => node.flowNodeType === FlowNodeTypeEnum.pluginInput)
?.inputs ?? []
pluginInputs
}
}
});

View File

@@ -43,6 +43,10 @@ async function handler(req: ApiRequestProps<InitTeamChatProps>, res: NextApiResp
// get app and history
const { nodes, chatConfig } = await getAppLatestVersion(app._id, app);
const pluginInputs =
chat?.pluginInputs ??
nodes?.find((node) => node.flowNodeType === FlowNodeTypeEnum.pluginInput)?.inputs ??
[];
jsonRes<InitChatResponse>(res, {
data: {
@@ -64,9 +68,7 @@ async function handler(req: ApiRequestProps<InitTeamChatProps>, res: NextApiResp
avatar: app.avatar,
intro: app.intro,
type: app.type,
pluginInputs:
app?.modules?.find((node) => node.flowNodeType === FlowNodeTypeEnum.pluginInput)
?.inputs ?? []
pluginInputs
}
}
});

View File

@@ -1,4 +1,6 @@
import { getFeishuAndYuqueDatasetFileList } from '@/service/core/dataset/apiDataset/controller';
import { NextAPI } from '@/service/middleware/entry';
import { DatasetErrEnum } from '@fastgpt/global/common/error/code/dataset';
import { ParentIdType } from '@fastgpt/global/common/parentFolder/type';
import { APIFileItem } from '@fastgpt/global/core/dataset/apiDataset';
import { ReadPermissionVal } from '@fastgpt/global/support/permission/constant';
@@ -26,11 +28,21 @@ async function handler(req: NextApiRequest) {
});
const apiServer = dataset.apiServer;
if (!apiServer) {
return Promise.reject('apiServer is required');
const feishuServer = dataset.feishuServer;
const yuqueServer = dataset.yuqueServer;
if (apiServer) {
return useApiDatasetRequest({ apiServer }).listFiles({ searchKey, parentId });
}
if (feishuServer || yuqueServer) {
return getFeishuAndYuqueDatasetFileList({
feishuServer,
yuqueServer,
parentId
});
}
return useApiDatasetRequest({ apiServer }).listFiles({ searchKey, parentId });
return Promise.reject(DatasetErrEnum.noApiServer);
}
export default NextAPI(handler);

View File

@@ -34,12 +34,8 @@ async function handler(req: NextApiRequest): CreateCollectionResponse {
});
const apiServer = dataset.apiServer;
if (!apiServer) {
return Promise.reject('Api server not found');
}
if (!apiFileId) {
return Promise.reject('ApiFileId not found');
}
const feishuServer = dataset.feishuServer;
const yuqueServer = dataset.yuqueServer;
// Auth same apiFileId
const storeCol = await MongoDatasetCollection.findOne(
@@ -57,6 +53,8 @@ async function handler(req: NextApiRequest): CreateCollectionResponse {
const content = await readApiServerFileContent({
apiServer,
feishuServer,
yuqueServer,
apiFileId,
teamId
});

View File

@@ -66,7 +66,9 @@ async function handler(
return {
type: DatasetSourceReadTypeEnum.apiFile,
sourceId: collection.apiFileId,
apiServer: collection.datasetId.apiServer
apiServer: collection.datasetId.apiServer,
feishuServer: collection.datasetId.feishuServer,
yuqueServer: collection.datasetId.yuqueServer
};
}
if (collection.type === DatasetCollectionTypeEnum.externalFile) {

View File

@@ -12,6 +12,7 @@ import { AIChatItemType, ChatHistoryItemResType } from '@fastgpt/global/core/cha
import { authChatCrud } from '@/service/support/permission/auth/chat';
import { getCollectionWithDataset } from '@fastgpt/service/core/dataset/controller';
import { useApiDatasetRequest } from '@fastgpt/service/core/dataset/apiDataset/api';
import { POST } from '@fastgpt/service/common/api/plusRequest';
export type readCollectionSourceQuery = {};
@@ -148,11 +149,25 @@ async function handler(
}
if (collection.type === DatasetCollectionTypeEnum.apiFile && collection.apiFileId) {
const apiServer = collection.datasetId.apiServer;
if (!apiServer) return Promise.reject('apiServer not found');
const feishuServer = collection.datasetId.feishuServer;
const yuqueServer = collection.datasetId.yuqueServer;
return useApiDatasetRequest({ apiServer }).getFilePreviewUrl({
apiFileId: collection.apiFileId
});
if (apiServer) {
return useApiDatasetRequest({ apiServer }).getFilePreviewUrl({
apiFileId: collection.apiFileId
});
}
if (feishuServer || yuqueServer) {
return POST<string>(`/core/dataset/systemApiDataset`, {
type: 'read',
apiFileId: collection.apiFileId,
feishuServer,
yuqueServer
});
}
return '';
}
if (collection.type === DatasetCollectionTypeEnum.externalFile) {
if (collection.externalFileId && collection.datasetId.externalReadUrl) {

View File

@@ -10,6 +10,7 @@ import { DatasetErrEnum } from '@fastgpt/global/common/error/code/dataset';
import type { ApiRequestProps } from '@fastgpt/service/type/next';
import { parseParentIdInMongo } from '@fastgpt/global/common/parentFolder/utils';
import { authDataset } from '@fastgpt/service/support/permission/dataset/auth';
import { pushTrack } from '@fastgpt/service/common/middle/tracks/utils';
export type DatasetCreateQuery = {};
export type DatasetCreateBody = CreateDatasetParams;
@@ -26,11 +27,13 @@ async function handler(
avatar,
vectorModel = global.vectorModels[0].model,
agentModel = getDatasetModel().model,
apiServer
apiServer,
feishuServer,
yuqueServer
} = req.body;
// auth
const [{ teamId, tmbId }] = await Promise.all([
const [{ teamId, tmbId, userId }] = await Promise.all([
authUserPer({
req,
authToken: true,
@@ -70,7 +73,16 @@ async function handler(
agentModel,
avatar,
type,
apiServer
apiServer,
feishuServer,
yuqueServer
});
pushTrack.createDataset({
type,
teamId,
tmbId,
uid: userId
});
return _id;

View File

@@ -36,6 +36,19 @@ async function handler(req: ApiRequestProps<Query>): Promise<DatasetItemType> {
authorization: ''
}
: undefined,
yuqueServer: dataset.yuqueServer
? {
userId: dataset.yuqueServer.userId,
token: ''
}
: undefined,
feishuServer: dataset.feishuServer
? {
appId: dataset.feishuServer.appId,
appSecret: '',
folderToken: dataset.feishuServer.folderToken
}
: undefined,
permission,
vectorModel: getVectorModel(dataset.vectorModel),
agentModel: getLLMModel(dataset.agentModel)

View File

@@ -50,7 +50,7 @@ async function handler(
throw new Error('chunkSize is too large, should be less than 30000');
}
const { teamId, apiServer } = await (async () => {
const { teamId, apiServer, feishuServer, yuqueServer } = await (async () => {
if (type === DatasetSourceReadTypeEnum.fileLocal) {
const res = await authCollectionFile({
req,
@@ -72,7 +72,9 @@ async function handler(
});
return {
teamId: dataset.teamId,
apiServer: dataset.apiServer
apiServer: dataset.apiServer,
feishuServer: dataset.feishuServer,
yuqueServer: dataset.yuqueServer
};
})();
@@ -83,6 +85,8 @@ async function handler(
selector,
isQAImport,
apiServer,
feishuServer,
yuqueServer,
externalFileId
});

View File

@@ -57,6 +57,8 @@ async function handler(
websiteConfig,
externalReadUrl,
apiServer,
yuqueServer,
feishuServer,
status,
autoSync
} = req.body;
@@ -124,6 +126,13 @@ async function handler(
...(!!apiServer?.authorization && {
'apiServer.authorization': apiServer.authorization
}),
...(!!yuqueServer?.userId && { 'yuqueServer.userId': yuqueServer.userId }),
...(!!yuqueServer?.token && { 'yuqueServer.token': yuqueServer.token }),
...(!!feishuServer?.appId && { 'feishuServer.appId': feishuServer.appId }),
...(!!feishuServer?.appSecret && { 'feishuServer.appSecret': feishuServer.appSecret }),
...(!!feishuServer?.folderToken && {
'feishuServer.folderToken': feishuServer.folderToken
}),
...(isMove && { inheritPermission: true }),
...(typeof autoSync === 'boolean' && { autoSync })
},

View File

@@ -0,0 +1,27 @@
import type { ApiRequestProps, ApiResponseType } from '@fastgpt/service/type/next';
import { POST } from '@fastgpt/service/common/api/plusRequest';
import { NextAPI } from '@/service/middleware/entry';
export type OutLinkDingtalkQuery = any;
export type OutLinkDingtalkBody = any;
export type OutLinkFeishuResponse = {};
async function handler(
req: ApiRequestProps<OutLinkDingtalkBody, OutLinkDingtalkQuery>,
res: ApiResponseType<any>
): Promise<any> {
if (req.method === 'GET') {
return {
success: true
};
}
// send to pro
const { token } = req.query;
const result = await POST<any>(`support/outLink/dingtalk/${token}`, req.body, {
headers: req.headers as any
});
return result;
}
export default NextAPI(handler);

View File

@@ -9,13 +9,11 @@ async function handler(
req: ApiRequestProps<OutLinkOffiAccountBody, OutLinkOffiAccountQuery>,
res: ApiResponseType<any>
): Promise<any> {
const { token, type } = req.query;
const { token } = req.query;
const result = await plusRequest({
method: req.method,
url: `support/outLink/offiaccount/${token}`,
params: {
...req.query,
type
},
params: req.query,
data: req.body
});

View File

@@ -6,6 +6,7 @@ import type { PostLoginProps } from '@fastgpt/global/support/user/api.d';
import { UserStatusEnum } from '@fastgpt/global/support/user/constant';
import { NextAPI } from '@/service/middleware/entry';
import { useReqFrequencyLimit } from '@fastgpt/service/common/middle/reqFrequencyLimit';
import { pushTrack } from '@fastgpt/service/common/middle/tracks/utils';
async function handler(req: NextApiRequest, res: NextApiResponse) {
const { username, password } = req.body as PostLoginProps;
@@ -47,6 +48,13 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
lastLoginTmbId: userDetail.team.tmbId
});
pushTrack.login({
type: 'password',
uid: user._id,
teamId: userDetail.team.teamId,
tmbId: userDetail.team.tmbId
});
const token = createJWT({
...userDetail,
isRoot: username === 'root'

View File

@@ -10,7 +10,6 @@ import dynamic from 'next/dynamic';
import LightRowTabs from '@fastgpt/web/components/common/Tabs/LightRowTabs';
import { PluginRunBoxTabEnum } from '@/components/core/chat/ChatContainer/PluginRunBox/constants';
import CloseIcon from '@fastgpt/web/components/common/Icon/close';
import ChatBox from '@/components/core/chat/ChatContainer/ChatBox';
import { useSystem } from '@fastgpt/web/hooks/useSystem';
import { PcHeader } from '@/pages/chat/components/ChatHeader';
import { GetChatTypeEnum } from '@/global/core/chat/constants';
@@ -22,6 +21,7 @@ import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { useContextSelector } from 'use-context-selector';
const PluginRunBox = dynamic(() => import('@/components/core/chat/ChatContainer/PluginRunBox'));
const ChatBox = dynamic(() => import('@/components/core/chat/ChatContainer/ChatBox'));
type Props = {
appId: string;
@@ -148,10 +148,12 @@ const DetailLogsModal = ({ appId, chatId, onClose }: Props) => {
)}
{/* Chat container */}
<Box pt={2} flex={'1 0 0'}>
<Box pt={2} flex={'1 0 0'} h={0}>
{isPlugin ? (
<Box px={5} pt={2} h={'100%'}>
<PluginRunBox appId={appId} chatId={chatId} />
<Box h={'100%'} overflow={'auto'}>
<Box px={5} py={2}>
<PluginRunBox appId={appId} chatId={chatId} />
</Box>
</Box>
) : (
<ChatBox

View File

@@ -0,0 +1,152 @@
import React from 'react';
import { Flex, Box, Button, ModalBody, Input, Link } from '@chakra-ui/react';
import MyModal from '@fastgpt/web/components/common/MyModal';
import { PublishChannelEnum } from '@fastgpt/global/support/outLink/constant';
import type { DingtalkAppType, OutLinkEditType } from '@fastgpt/global/support/outLink/type';
import { useTranslation } from 'next-i18next';
import { useForm } from 'react-hook-form';
import { createShareChat, updateShareChat } from '@/web/support/outLink/api';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import BasicInfo from '../components/BasicInfo';
import { getDocPath } from '@/web/common/system/doc';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import MyIcon from '@fastgpt/web/components/common/Icon';
import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel';
const DingTalkEditModal = ({
appId,
defaultData,
onClose,
onCreate,
onEdit,
isEdit = false
}: {
appId: string;
defaultData: OutLinkEditType<DingtalkAppType>;
onClose: () => void;
onCreate: (id: string) => void;
onEdit: () => void;
isEdit?: boolean;
}) => {
const { t } = useTranslation();
const {
register,
setValue,
handleSubmit: submitShareChat
} = useForm({
defaultValues: defaultData
});
const { runAsync: onclickCreate, loading: creating } = useRequest2(
(e: Omit<OutLinkEditType<DingtalkAppType>, 'appId' | 'type'>) =>
createShareChat({
...e,
appId,
type: PublishChannelEnum.dingtalk,
app: {
clientId: e?.app?.clientId?.trim(),
clientSecret: e.app?.clientSecret?.trim()
}
}),
{
errorToast: t('common:common.Create Failed'),
successToast: t('common:common.Create Success'),
onSuccess: onCreate
}
);
const { runAsync: onclickUpdate, loading: updating } = useRequest2(
(e) =>
updateShareChat({
...e,
app: {
clientId: e?.app?.clientId?.trim(),
clientSecret: e.app?.clientSecret?.trim()
}
}),
{
errorToast: t('common:common.Update Failed'),
successToast: t('common:common.Update Success'),
onSuccess: onEdit
}
);
const { feConfigs } = useSystemStore();
return (
<MyModal
iconSrc="common/dingtalkFill"
title={
isEdit ? t('publish:dingtalk.edit_modal_title') : t('publish:dingtalk.create_modal_title')
}
minW={['auto', '60rem']}
>
<ModalBody display={'grid'} gridTemplateColumns={['1fr', '1fr 1fr']} fontSize={'14px'} p={0}>
<Box p={8} h={['auto', '400px']} borderRight={'base'}>
<BasicInfo register={register} setValue={setValue} defaultData={defaultData} />
</Box>
<Flex p={8} h={['auto', '400px']} flexDirection="column" gap={6}>
<Flex alignItems="center">
<Box color="myGray.600">{t('publish:dingtalk.api')}</Box>
{feConfigs?.docUrl && (
<Link
href={
feConfigs.openAPIDocUrl ||
getDocPath('/docs/use-cases/external-integration/dingtalk/')
}
target={'_blank'}
ml={2}
color={'primary.500'}
fontSize={'sm'}
>
<Flex alignItems={'center'}>
<MyIcon w={'17px'} h={'17px'} name="book" mr="1" />
{t('common:common.Read document')}
</Flex>
</Link>
)}
</Flex>
<Flex alignItems={'center'}>
<FormLabel flex={'0 0 6.25rem'} required>
Client ID
</FormLabel>
<Input
placeholder={'Client ID'}
{...register('app.clientId', {
required: true
})}
/>
</Flex>
<Flex alignItems={'center'}>
<FormLabel flex={'0 0 6.25rem'} required>
Client Secret
</FormLabel>
<Input
placeholder={'Client Secret'}
{...register('app.clientSecret', {
required: true
})}
/>
</Flex>
<Box flex={1}></Box>
<Flex justifyContent={'end'}>
<Button variant={'whiteBase'} mr={3} onClick={onClose}>
{t('common:common.Close')}
</Button>
<Button
isLoading={creating || updating}
onClick={submitShareChat((data) =>
isEdit ? onclickUpdate(data) : onclickCreate(data)
)}
>
{t('common:common.Confirm')}
</Button>
</Flex>
</Flex>
</ModalBody>
</MyModal>
);
};
export default DingTalkEditModal;

View File

@@ -0,0 +1,248 @@
import React, { useMemo, useState } from 'react';
import {
Flex,
Box,
Button,
TableContainer,
Table,
Thead,
Tr,
Th,
Td,
Tbody,
useDisclosure,
Link,
HStack
} from '@chakra-ui/react';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { useLoading } from '@fastgpt/web/hooks/useLoading';
import { getShareChatList, delShareChatById } from '@/web/support/outLink/api';
import { formatTimeToChatTime } from '@fastgpt/global/common/string/time';
import { defaultDingtalkOutlinkForm } from '@/web/core/app/constants';
import type { DingtalkAppType, OutLinkEditType } from '@fastgpt/global/support/outLink/type.d';
import { PublishChannelEnum } from '@fastgpt/global/support/outLink/constant';
import { useTranslation } from 'next-i18next';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import dayjs from 'dayjs';
import dynamic from 'next/dynamic';
import MyMenu from '@fastgpt/web/components/common/MyMenu';
import EmptyTip from '@fastgpt/web/components/common/EmptyTip';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { getDocPath } from '@/web/common/system/doc';
const DingTalkEditModal = dynamic(() => import('./DingTalkEditModal'));
const ShowShareLinkModal = dynamic(() => import('../components/showShareLinkModal'));
const DingTalk = ({ appId }: { appId: string }) => {
const { t } = useTranslation();
const { Loading, setIsLoading } = useLoading();
const { feConfigs } = useSystemStore();
const [editDingTalkLinkData, setEditDingTalkLinkData] =
useState<OutLinkEditType<DingtalkAppType>>();
const [isEdit, setIsEdit] = useState<boolean>(false);
const baseUrl = useMemo(
() => feConfigs?.customApiDomain || `${location.origin}/api`,
[feConfigs?.customApiDomain]
);
const {
data: shareChatList = [],
loading: isFetching,
runAsync: refetchShareChatList
} = useRequest2(
() => getShareChatList<DingtalkAppType>({ appId, type: PublishChannelEnum.dingtalk }),
{
manual: false
}
);
const {
onOpen: openShowShareLinkModal,
isOpen: showShareLinkModalOpen,
onClose: closeShowShareLinkModal
} = useDisclosure();
const [showShareLink, setShowShareLink] = useState<string | null>(null);
return (
<Box position={'relative'} pt={3} px={5} minH={'50vh'}>
<Flex justifyContent={'space-between'} flexDirection="row">
<HStack>
<Box fontWeight={'bold'} fontSize={['md', 'lg']}>
{t('publish:dingtalk.title')}
</Box>
{feConfigs?.docUrl && (
<Link
href={
feConfigs.openAPIDocUrl ||
getDocPath('/docs/use-cases/external-integration/dingtalk/')
}
target={'_blank'}
color={'primary.500'}
fontSize={'sm'}
>
<Flex alignItems={'center'}>
<MyIcon name="book" mr="1" w={'1rem'} />
{t('common:common.Read document')}
</Flex>
</Link>
)}
</HStack>
<Button
variant={'primary'}
colorScheme={'blue'}
size={['sm', 'md']}
leftIcon={<MyIcon name={'common/addLight'} w="1.25rem" color="white" />}
ml={3}
{...(shareChatList.length >= 10
? {
isDisabled: true,
title: t('common:core.app.share.Amount limit tip')
}
: {})}
onClick={() => {
setEditDingTalkLinkData(defaultDingtalkOutlinkForm);
setIsEdit(false);
}}
>
{t('common:add_new')}
</Button>
</Flex>
<TableContainer mt={3}>
<Table variant={'simple'} w={'100%'} overflowX={'auto'} fontSize={'sm'}>
<Thead>
<Tr>
<Th>{t('common:common.Name')}</Th>
<Th>{t('common:support.outlink.Usage points')}</Th>
{feConfigs?.isPlus && (
<>
<Th>{t('common:core.app.share.Ip limit title')}</Th>
<Th>{t('common:common.Expired Time')}</Th>
</>
)}
<Th>{t('common:common.Last use time')}</Th>
<Th></Th>
</Tr>
</Thead>
<Tbody>
{shareChatList.map((item) => (
<Tr key={item._id}>
<Td>{item.name}</Td>
<Td>
{Math.round(item.usagePoints)}
{feConfigs?.isPlus
? `${
item.limit?.maxUsagePoints && item.limit.maxUsagePoints > -1
? ` / ${item.limit.maxUsagePoints}`
: ` / ${t('common:common.Unlimited')}`
}`
: ''}
</Td>
{feConfigs?.isPlus && (
<>
<Td>{item?.limit?.QPM || '-'}</Td>
<Td>
{item?.limit?.expiredTime
? dayjs(item.limit?.expiredTime).format('YYYY/MM/DD\nHH:mm')
: '-'}
</Td>
</>
)}
<Td>
{item.lastTime
? t(formatTimeToChatTime(item.lastTime) as any).replace('#', ':')
: t('common:common.Un used')}
</Td>
<Td display={'flex'} alignItems={'center'}>
<Button
onClick={() => {
setShowShareLink(`${baseUrl}/support/outLink/dingtalk/${item.shareId}`);
openShowShareLinkModal();
}}
size={'sm'}
mr={3}
variant={'whitePrimary'}
>
{t('publish:request_address')}
</Button>
<MyMenu
Button={
<MyIcon
name={'more'}
_hover={{ bg: 'myGray.100' }}
cursor={'pointer'}
borderRadius={'md'}
w={'14px'}
p={2}
/>
}
menuList={[
{
children: [
{
label: t('common:common.Edit'),
icon: 'edit',
onClick: () => {
setEditDingTalkLinkData({
_id: item._id,
name: item.name,
limit: item.limit,
app: item.app,
responseDetail: item.responseDetail,
defaultResponse: item.defaultResponse,
immediateResponse: item.immediateResponse
});
setIsEdit(true);
}
},
{
label: t('common:common.Delete'),
icon: 'delete',
onClick: async () => {
setIsLoading(true);
try {
await delShareChatById(item._id);
refetchShareChatList();
} catch (error) {
console.log(error);
}
setIsLoading(false);
}
}
]
}
]}
/>
</Td>
</Tr>
))}
</Tbody>
</Table>
</TableContainer>
{editDingTalkLinkData && (
<DingTalkEditModal
appId={appId}
defaultData={editDingTalkLinkData}
onCreate={() => Promise.all([refetchShareChatList(), setEditDingTalkLinkData(undefined)])}
onEdit={() => Promise.all([refetchShareChatList(), setEditDingTalkLinkData(undefined)])}
onClose={() => setEditDingTalkLinkData(undefined)}
isEdit={isEdit}
/>
)}
{shareChatList.length === 0 && !isFetching && (
<EmptyTip text={t('common:core.app.share.Not share link')}></EmptyTip>
)}
<Loading loading={isFetching} fixed={false} />
{showShareLinkModalOpen && (
<ShowShareLinkModal
shareLink={showShareLink ?? ''}
onClose={closeShowShareLinkModal}
img="/imgs/outlink/dingtalk-copylink-instruction.png"
/>
)}
</Box>
);
};
export default React.memo(DingTalk);

View File

@@ -16,6 +16,7 @@ import { useToast } from '@fastgpt/web/hooks/useToast';
const Link = dynamic(() => import('./Link'));
const API = dynamic(() => import('./API'));
const FeiShu = dynamic(() => import('./FeiShu'));
const DingTalk = dynamic(() => import('./DingTalk'));
// const Wecom = dynamic(() => import('./Wecom'));
const OffiAccount = dynamic(() => import('./OffiAccount'));
@@ -48,6 +49,13 @@ const OutLink = () => {
value: PublishChannelEnum.feishu,
isProFn: true
},
{
icon: 'common/dingtalkFill',
title: t('publish:dingtalk.bot'),
desc: t('publish:dingtalk.bot_desc'),
value: PublishChannelEnum.dingtalk,
isProFn: true
},
// {
// icon: 'core/app/publish/wecom',
// title: t('publish:wecom.bot'),
@@ -114,6 +122,7 @@ const OutLink = () => {
)}
{linkType === PublishChannelEnum.apikey && <API appId={appId} />}
{linkType === PublishChannelEnum.feishu && <FeiShu appId={appId} />}
{linkType === PublishChannelEnum.dingtalk && <DingTalk appId={appId} />}
{/* {linkType === PublishChannelEnum.wecom && <Wecom appId={appId} />} */}
{linkType === PublishChannelEnum.officialAccount && <OffiAccount appId={appId} />}
</Flex>

View File

@@ -1,5 +1,5 @@
import { Box, Flex } from '@chakra-ui/react';
import React from 'react';
import React, { useMemo } from 'react';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { useTranslation } from 'next-i18next';
import { nodeTemplate2FlowNode } from '@/web/core/workflow/utils';
@@ -8,12 +8,14 @@ import { useContextSelector } from 'use-context-selector';
import { useReactFlow } from 'reactflow';
import { WorkflowNodeEdgeContext } from '../../context/workflowInitContext';
import { WorkflowEventContext } from '../../context/workflowEventContext';
import { WorkflowContext } from '../../context';
const ContextMenu = () => {
const { t } = useTranslation();
const setNodes = useContextSelector(WorkflowNodeEdgeContext, (v) => v.setNodes);
const menu = useContextSelector(WorkflowEventContext, (v) => v.menu);
const setMenu = useContextSelector(WorkflowEventContext, (ctx) => ctx.setMenu);
const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList);
const { screenToFlowPosition } = useReactFlow();
const newNode = nodeTemplate2FlowNode({
@@ -22,38 +24,44 @@ const ContextMenu = () => {
t
});
return (
!!menu && (
<Box position="relative">
<Box
position="absolute"
top={`${menu.top - 6}px`}
left={`${menu.left + 10}px`}
width={0}
height={0}
borderLeft="6px solid transparent"
borderRight="6px solid transparent"
borderBottom="6px solid white"
zIndex={2}
filter="drop-shadow(0px -1px 2px rgba(0, 0, 0, 0.1))"
/>
const allUnFolded = useMemo(() => {
return !!menu ? nodeList.some((node) => node.isFolded) : false;
}, [nodeList, menu]);
return !!menu ? (
<Box position="relative">
<Box
position="absolute"
top={`${menu.top - 6}px`}
left={`${menu.left + 10}px`}
width={0}
height={0}
borderLeft="6px solid transparent"
borderRight="6px solid transparent"
borderBottom="6px solid white"
zIndex={2}
filter="drop-shadow(0px -1px 2px rgba(0, 0, 0, 0.1))"
/>
<Box
position={'absolute'}
top={menu.top}
left={menu.left}
bg={'white'}
w={'120px'}
rounded={'md'}
boxShadow={'0px 2px 4px 0px #A1A7B340'}
className="context-menu"
color={'myGray.600'}
p={1}
zIndex={10}
>
<Flex
position={'absolute'}
top={menu.top}
left={menu.left}
bg={'white'}
w={'120px'}
height={9}
p={1}
rounded={'md'}
boxShadow={'0px 2px 4px 0px #A1A7B340'}
className="context-menu"
alignItems={'center'}
color={'myGray.600'}
px={2}
py={1}
cursor={'pointer'}
_hover={{
color: 'primary.500'
}}
borderRadius={'sm'}
_hover={{ bg: 'myGray.50', color: 'primary.500' }}
onClick={() => {
setMenu(null);
setNodes((state) => {
@@ -67,16 +75,41 @@ const ContextMenu = () => {
return newState;
});
}}
zIndex={1}
>
<MyIcon name="comment" w={'16px'} h={'16px'} ml={1} />
<MyIcon name="comment" w={'1rem'} ml={1} />
<Box fontSize={'12px'} fontWeight={'500'} ml={1.5}>
{t('workflow:context_menu.add_comment')}
</Box>
</Flex>
<Flex
mt={1}
alignItems={'center'}
px={2}
py={1}
cursor={'pointer'}
borderRadius={'sm'}
_hover={{ bg: 'myGray.50', color: 'primary.500' }}
onClick={() => {
setMenu(null);
setNodes((state) => {
return state.map((node) => ({
...node,
data: {
...node.data,
isFolded: !allUnFolded
}
}));
});
}}
>
<MyIcon name="common/select" w={'1rem'} ml={1} />
<Box fontSize={'12px'} fontWeight={'500'} ml={1.5}>
{allUnFolded ? t('workflow:unFoldAll') : t('workflow:foldAll')}
</Box>
</Flex>
</Box>
)
);
</Box>
) : null;
};
export default React.memo(ContextMenu);

View File

@@ -126,7 +126,6 @@ const Workflow = () => {
<NodeTemplatesModal isOpen={isOpenTemplate} onClose={onCloseTemplate} />
</>
<ContextMenu />
<ReactFlow
ref={reactFlowWrapper}
fitView
@@ -162,6 +161,7 @@ const Workflow = () => {
: {})}
onNodeDragStop={onNodeDragStop}
>
<ContextMenu />
<FlowController />
<HelperLines horizontal={helperLineHorizontal} vertical={helperLineVertical} />
</ReactFlow>

View File

@@ -282,7 +282,7 @@ const NodeCard = (props: Props) => {
</MyTooltip>
)}
</Flex>
{intro && <NodeIntro nodeId={nodeId} intro={intro} />}
<NodeIntro nodeId={nodeId} intro={intro} />
</Box>
)}
<MenuRender nodeId={nodeId} menuForbid={menuForbid} nodeList={nodeList} />
@@ -580,7 +580,7 @@ const NodeIntro = React.memo(function NodeIntro({
<>
<Flex alignItems={'center'}>
<Box fontSize={'sm'} color={'myGray.500'} flex={'1 0 0'}>
{t(intro as any)}
{t(intro as any) || t('app:node_not_intro')}
</Box>
{NodeIsTool && (
<Flex

View File

@@ -33,6 +33,7 @@ import EmptyTip from '@fastgpt/web/components/common/EmptyTip';
import SearchInput from '../../../../../../../packages/web/components/common/Input/SearchInput/index';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import { webPushTrack } from '@/web/common/middle/tracks/utils';
type TemplateAppType = AppTypeEnum | 'all';
@@ -98,6 +99,13 @@ const TemplateMarketModal = ({
modules: templateDetail.workflow.nodes || [],
edges: templateDetail.workflow.edges || [],
chatConfig: templateDetail.workflow.chatConfig
}).then((res) => {
webPushTrack.useAppTemplate({
id,
name: templateDetail.name
});
return res;
});
},
{

View File

@@ -0,0 +1,166 @@
import React from 'react';
import { DatasetTypeEnum } from '@fastgpt/global/core/dataset/constants';
import { Flex, Input } from '@chakra-ui/react';
import { UseFormReturn } from 'react-hook-form';
import { useTranslation } from 'next-i18next';
import type {
APIFileServer,
FeishuServer,
YuqueServer
} from '@fastgpt/global/core/dataset/apiDataset';
const ApiDatasetForm = ({
type,
form
}: {
type: `${DatasetTypeEnum}`;
form: UseFormReturn<
{
apiServer?: APIFileServer;
feishuServer?: FeishuServer;
yuqueServer?: YuqueServer;
},
any
>;
}) => {
const { t } = useTranslation();
const { register } = form;
return (
<>
{type === DatasetTypeEnum.apiDataset && (
<>
<Flex mt={6}>
<Flex
alignItems={'center'}
flex={['', '0 0 110px']}
color={'myGray.900'}
fontWeight={500}
fontSize={'sm'}
>
{t('dataset:api_url')}
</Flex>
<Input
bg={'myWhite.600'}
placeholder={t('dataset:api_url')}
maxLength={200}
{...register('apiServer.baseUrl', { required: true })}
/>
</Flex>
<Flex mt={6}>
<Flex
alignItems={'center'}
flex={['', '0 0 110px']}
color={'myGray.900'}
fontWeight={500}
fontSize={'sm'}
>
Authorization
</Flex>
<Input
bg={'myWhite.600'}
placeholder={t('dataset:request_headers')}
maxLength={200}
{...register('apiServer.authorization')}
/>
</Flex>
</>
)}
{type === DatasetTypeEnum.feishu && (
<>
<Flex mt={6}>
<Flex
alignItems={'center'}
flex={['', '0 0 110px']}
color={'myGray.900'}
fontWeight={500}
fontSize={'sm'}
>
App ID
</Flex>
<Input
bg={'myWhite.600'}
placeholder={'App ID'}
maxLength={200}
{...register('feishuServer.appId', { required: true })}
/>
</Flex>
<Flex mt={6}>
<Flex
alignItems={'center'}
flex={['', '0 0 110px']}
color={'myGray.900'}
fontWeight={500}
fontSize={'sm'}
>
App Secret
</Flex>
<Input
bg={'myWhite.600'}
placeholder={'App Secret'}
maxLength={200}
{...register('feishuServer.appSecret', { required: true })}
/>
</Flex>
<Flex mt={6}>
<Flex
alignItems={'center'}
flex={['', '0 0 110px']}
color={'myGray.900'}
fontWeight={500}
fontSize={'sm'}
>
Folder Token
</Flex>
<Input
bg={'myWhite.600'}
placeholder={'Folder Token'}
maxLength={200}
{...register('feishuServer.folderToken', { required: true })}
/>
</Flex>
</>
)}
{type === DatasetTypeEnum.yuque && (
<>
<Flex mt={6}>
<Flex
alignItems={'center'}
flex={['', '0 0 110px']}
color={'myGray.900'}
fontWeight={500}
fontSize={'sm'}
>
User ID
</Flex>
<Input
bg={'myWhite.600'}
placeholder={'Token'}
maxLength={200}
{...register('yuqueServer.userId', { required: true })}
/>
</Flex>
<Flex mt={6}>
<Flex
alignItems={'center'}
flex={['', '0 0 110px']}
color={'myGray.900'}
fontWeight={500}
fontSize={'sm'}
>
Token
</Flex>
<Input
bg={'myWhite.600'}
placeholder={'Token'}
maxLength={200}
{...register('yuqueServer.token', { required: true })}
/>
</Flex>
</>
)}
</>
);
};
export default ApiDatasetForm;

View File

@@ -386,7 +386,9 @@ const Header = ({}: {}) => {
/>
)}
{/* apiDataset */}
{datasetDetail?.type === DatasetTypeEnum.apiDataset && (
{(datasetDetail?.type === DatasetTypeEnum.apiDataset ||
datasetDetail?.type === DatasetTypeEnum.feishu ||
datasetDetail?.type === DatasetTypeEnum.yuque) && (
<Flex
px={3.5}
py={2}

View File

@@ -177,8 +177,7 @@ const Upload = () => {
router.replace({
query: {
datasetId: datasetDetail._id,
currentTab: retrainNewCollectionId.current ? TabEnum.dataCard : TabEnum.collectionCard,
collectionId: retrainNewCollectionId.current
currentTab: TabEnum.collectionCard
}
});
},

View File

@@ -49,23 +49,26 @@ const CustomAPIFileInput = () => {
parentId: '',
parentName: ''
});
const [parentUuid, setParentUuid] = useState<string>('');
const [paths, setPaths] = useState<ParentTreePathItemType[]>([]);
const [searchKey, setSearchKey] = useState('');
const { data: fileList = [], loading } = useRequest2(
async () =>
datasetDetail?.apiServer
? getApiDatasetFileList({
datasetId: datasetDetail._id,
parentId: parent?.parentId,
searchKey: searchKey
})
: [],
async () => {
return getApiDatasetFileList({
datasetId: datasetDetail._id,
parentId: parent?.parentId,
searchKey: searchKey
});
},
{
refreshDeps: [datasetDetail._id, datasetDetail.apiServer, parent, searchKey],
throttleWait: 500,
manual: false
}
);
const { data: existIdList = [] } = useRequest2(
() => getApiDatasetFileListExistId({ datasetId: datasetDetail._id }),
{
@@ -90,6 +93,7 @@ const CustomAPIFileInput = () => {
datasetId: datasetDetail._id,
parentId: file?.id
});
const subFiles = await getFilesRecursively(folderFiles);
allFiles.push(...subFiles);
} else {
@@ -125,6 +129,7 @@ const CustomAPIFileInput = () => {
const handleItemClick = useCallback(
(item: APIFileItem) => {
if (item.type === 'folder') {
setPaths((state) => [...state, { parentId: item.id, parentName: item.name }]);
return setParent({
parentId: item.id,
parentName: item.name
@@ -138,7 +143,7 @@ const CustomAPIFileInput = () => {
setSelectFiles((state) => [...state, item]);
}
},
[selectFiles, setSelectFiles]
[selectFiles]
);
const handleSelectAll = useCallback(() => {
@@ -151,8 +156,6 @@ const CustomAPIFileInput = () => {
}
}, [fileList, selectFiles]);
const paths = useMemo(() => [parent || { parentId: '', parentName: '' }], [parent]);
return (
<MyBox isLoading={loading} position="relative" h="full">
<Flex flexDirection={'column'} h="full">
@@ -160,21 +163,21 @@ const CustomAPIFileInput = () => {
<FolderPath
paths={paths}
onClick={(parentId) => {
if (parentId !== parent?.parentId) {
setParent({
parentId,
parentName: ''
});
}
const index = paths.findIndex((item) => item.parentId === parentId);
setParent(paths[index]);
setPaths(paths.slice(0, index + 1));
}}
/>
<Box w={'240px'}>
<SearchInput
value={searchKey}
onChange={(e) => setSearchKey(e.target.value)}
placeholder={t('common:core.workflow.template.Search')}
/>
</Box>
{datasetDetail.apiServer && (
<Box w={'240px'}>
<SearchInput
value={searchKey}
onChange={(e) => setSearchKey(e.target.value)}
placeholder={t('common:core.workflow.template.Search')}
/>
</Box>
)}
</Flex>
<Box flex={1} overflowY="auto" mb={16}>
<Box ml={2} mt={3}>

View File

@@ -1,15 +1,23 @@
import React from 'react';
import { ModalFooter, ModalBody, Input, Button, Flex } from '@chakra-ui/react';
import { ModalFooter, ModalBody, Button, Flex } from '@chakra-ui/react';
import MyModal from '@fastgpt/web/components/common/MyModal/index';
import { useTranslation } from 'next-i18next';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { useForm } from 'react-hook-form';
import { useToast } from '@fastgpt/web/hooks/useToast';
import { APIFileServer } from '@fastgpt/global/core/dataset/apiDataset';
import { APIFileServer, FeishuServer, YuqueServer } from '@fastgpt/global/core/dataset/apiDataset';
import ApiDatasetForm from '@/pages/dataset/component/ApiDatasetForm';
import { useContextSelector } from 'use-context-selector';
import { DatasetPageContext } from '@/web/core/dataset/context/datasetPageContext';
import { datasetTypeCourseMap } from '@/web/core/dataset/constants';
import { getDocPath } from '@/web/common/system/doc';
import MyIcon from '@fastgpt/web/components/common/Icon';
export type EditAPIDatasetInfoFormType = {
id: string;
apiServer?: APIFileServer;
yuqueServer?: YuqueServer;
feishuServer?: FeishuServer;
};
const EditAPIDatasetInfoModal = ({
@@ -24,7 +32,11 @@ const EditAPIDatasetInfoModal = ({
}) => {
const { t } = useTranslation();
const { toast } = useToast();
const { register, handleSubmit } = useForm<EditAPIDatasetInfoFormType>({
const datasetDetail = useContextSelector(DatasetPageContext, (v) => v.datasetDetail);
const type = datasetDetail.type;
const form = useForm<EditAPIDatasetInfoFormType>({
defaultValues: defaultForm
});
@@ -44,43 +56,24 @@ const EditAPIDatasetInfoModal = ({
return (
<MyModal isOpen onClose={onClose} w={'450px'} iconSrc="modal/edit" title={title}>
<ModalBody>
<Flex>
{datasetTypeCourseMap[type] && (
<Flex
alignItems={'center'}
flex={['', '0 0 110px']}
color={'myGray.900'}
fontWeight={500}
justifyContent={'flex-end'}
color={'primary.600'}
fontSize={'sm'}
cursor={'pointer'}
onClick={() => window.open(getDocPath(datasetTypeCourseMap[type]), '_blank')}
>
{t('dataset:api_url')}
<MyIcon name={'book'} w={4} mr={0.5} />
{t('common:Instructions')}
</Flex>
<Input
bg={'myWhite.600'}
placeholder={t('dataset:api_url')}
maxLength={200}
{...register('apiServer.baseUrl', { required: true })}
/>
</Flex>
<Flex mt={6}>
<Flex
alignItems={'center'}
flex={['', '0 0 110px']}
color={'myGray.900'}
fontWeight={500}
fontSize={'sm'}
>
Authorization
</Flex>
<Input
bg={'myWhite.600'}
placeholder={t('dataset:request_headers')}
maxLength={200}
{...register('apiServer.authorization')}
/>
</Flex>
)}
{/* @ts-ignore */}
<ApiDatasetForm type={type} form={form} />
</ModalBody>
<ModalFooter>
<Button isLoading={loading} onClick={handleSubmit(onSave)} px={6}>
<Button isLoading={loading} onClick={form.handleSubmit(onSave)} px={6}>
{t('common:common.Confirm')}
</Button>
</ModalFooter>

View File

@@ -29,13 +29,12 @@ import {
} from '@/web/core/dataset/api/collaborator';
import DatasetTypeTag from '@/components/core/dataset/DatasetTypeTag';
import dynamic from 'next/dynamic';
import EditAPIDatasetInfoModal, {
EditAPIDatasetInfoFormType
} from './components/EditApiServiceModal';
import type { EditAPIDatasetInfoFormType } from './components/EditApiServiceModal';
import { EditResourceInfoFormType } from '@/components/common/Modal/EditResourceModal';
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
const EditResourceModal = dynamic(() => import('@/components/common/Modal/EditResourceModal'));
const EditAPIDatasetInfoModal = dynamic(() => import('./components/EditApiServiceModal'));
const Info = ({ datasetId }: { datasetId: string }) => {
const { t } = useTranslation();
@@ -326,6 +325,56 @@ const Info = ({ datasetId }: { datasetId: string }) => {
</Box>
</>
)}
{datasetDetail.type === DatasetTypeEnum.yuque && (
<>
<Box w={'100%'} alignItems={'center'} pt={4}>
<Flex justifyContent={'space-between'} mb={1}>
<FormLabel fontSize={'mini'} fontWeight={'500'}>
{t('dataset:yuque_dataset_config')}
</FormLabel>
<MyIcon
name={'edit'}
w={'14px'}
_hover={{ color: 'primary.600' }}
cursor={'pointer'}
onClick={() =>
setEditedAPIDataset({
id: datasetDetail._id,
yuqueServer: datasetDetail.yuqueServer
})
}
/>
</Flex>
<Box fontSize={'mini'}>{datasetDetail.yuqueServer?.userId}</Box>
</Box>
</>
)}
{datasetDetail.type === DatasetTypeEnum.feishu && (
<>
<Box w={'100%'} alignItems={'center'} pt={4}>
<Flex justifyContent={'space-between'} mb={1}>
<FormLabel fontSize={'mini'} fontWeight={'500'}>
{t('dataset:feishu_dataset_config')}
</FormLabel>
<MyIcon
name={'edit'}
w={'14px'}
_hover={{ color: 'primary.600' }}
cursor={'pointer'}
onClick={() =>
setEditedAPIDataset({
id: datasetDetail._id,
feishuServer: datasetDetail.feishuServer
})
}
/>
</Flex>
<Box fontSize={'mini'}>{datasetDetail.feishuServer?.folderToken}</Box>
</Box>
</>
)}
</Box>
{datasetDetail.permission.hasManagePer && (
@@ -384,12 +433,14 @@ const Info = ({ datasetId }: { datasetId: string }) => {
{editedAPIDataset && (
<EditAPIDatasetInfoModal
{...editedAPIDataset}
title={t('common:dataset.Edit API Service')}
title={t('dataset:edit_dataset_config')}
onClose={() => setEditedAPIDataset(undefined)}
onEdit={(data) =>
updateDataset({
id: datasetId,
apiServer: data.apiServer
apiServer: data.apiServer,
yuqueServer: data.yuqueServer,
feishuServer: data.feishuServer
})
}
/>

View File

@@ -22,11 +22,15 @@ import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
import ComplianceTip from '@/components/common/ComplianceTip/index';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { getDocPath } from '@/web/common/system/doc';
import { datasetTypeCourseMap } from '@/web/core/dataset/constants';
import ApiDatasetForm from '../../component/ApiDatasetForm';
export type CreateDatasetType =
| DatasetTypeEnum.dataset
| DatasetTypeEnum.apiDataset
| DatasetTypeEnum.websiteDataset;
| DatasetTypeEnum.websiteDataset
| DatasetTypeEnum.feishu
| DatasetTypeEnum.yuque;
const CreateModal = ({
onClose,
@@ -43,35 +47,45 @@ const CreateModal = ({
const { vectorModelList, datasetModelList } = useSystemStore();
const { isPc } = useSystem();
const databaseNameMap = useMemo(() => {
const datasetTypeMap = useMemo(() => {
return {
[DatasetTypeEnum.dataset]: t('dataset:common_dataset'),
[DatasetTypeEnum.apiDataset]: t('dataset:api_file'),
[DatasetTypeEnum.websiteDataset]: t('dataset:website_dataset')
[DatasetTypeEnum.dataset]: {
name: t('dataset:common_dataset'),
icon: 'core/dataset/commonDatasetColor'
},
[DatasetTypeEnum.apiDataset]: {
name: t('dataset:api_file'),
icon: 'core/dataset/externalDatasetColor'
},
[DatasetTypeEnum.websiteDataset]: {
name: t('dataset:website_dataset'),
icon: 'core/dataset/websiteDatasetColor'
},
[DatasetTypeEnum.feishu]: {
name: t('dataset:feishu_dataset'),
icon: 'core/dataset/feishuDatasetColor'
},
[DatasetTypeEnum.yuque]: {
name: t('dataset:yuque_dataset'),
icon: 'core/dataset/yuqueDatasetColor'
}
};
}, [t]);
const iconMap = useMemo(() => {
return {
[DatasetTypeEnum.dataset]: 'core/dataset/commonDatasetColor',
[DatasetTypeEnum.apiDataset]: 'core/dataset/externalDatasetColor',
[DatasetTypeEnum.websiteDataset]: 'core/dataset/websiteDatasetColor'
};
}, []);
const filterNotHiddenVectorModelList = vectorModelList.filter((item) => !item.hidden);
const { register, setValue, handleSubmit, watch } = useForm<CreateDatasetParams>({
const form = useForm<CreateDatasetParams>({
defaultValues: {
parentId,
type: type || DatasetTypeEnum.dataset,
avatar: iconMap[type] || 'core/dataset/commonDatasetColor',
avatar: datasetTypeMap[type].icon,
name: '',
intro: '',
vectorModel: filterNotHiddenVectorModelList[0].model,
agentModel: datasetModelList[0].model
}
});
const { register, setValue, handleSubmit, watch } = form;
const avatar = watch('avatar');
const vectorModel = watch('vectorModel');
const agentModel = watch('agentModel');
@@ -119,8 +133,14 @@ const CreateModal = ({
<MyModal
title={
<Flex alignItems={'center'} ml={-3}>
<Avatar w={'20px'} h={'20px'} borderRadius={'xs'} src={iconMap[type]} pr={'10px'} />
{t('common:core.dataset.Create dataset', { name: databaseNameMap[type] })}
<Avatar
w={'20px'}
h={'20px'}
borderRadius={'xs'}
src={datasetTypeMap[type].icon}
pr={'10px'}
/>
{t('common:core.dataset.Create dataset', { name: datasetTypeMap[type].name })}
</Flex>
}
isOpen
@@ -134,16 +154,14 @@ const CreateModal = ({
<Box color={'myGray.900'} fontWeight={500} fontSize={'sm'}>
{t('common:common.Set Name')}
</Box>
{type === DatasetTypeEnum.apiDataset && (
{datasetTypeCourseMap[type] && (
<Flex
as={'span'}
alignItems={'center'}
color={'primary.600'}
fontSize={'sm'}
cursor={'pointer'}
onClick={() =>
window.open(getDocPath('/docs/guide/knowledge_base/api_dataset/'), '_blank')
}
onClick={() => window.open(getDocPath(datasetTypeCourseMap[type]), '_blank')}
>
<MyIcon name={'book'} w={4} mr={0.5} />
{t('common:Instructions')}
@@ -242,44 +260,8 @@ const CreateModal = ({
</Box>
</Flex>
)}
{type === DatasetTypeEnum.apiDataset && (
<>
<Flex mt={6}>
<Flex
alignItems={'center'}
flex={['', '0 0 110px']}
color={'myGray.900'}
fontWeight={500}
fontSize={'sm'}
>
{t('dataset:api_url')}
</Flex>
<Input
bg={'myWhite.600'}
placeholder={t('dataset:api_url')}
maxLength={200}
{...register('apiServer.baseUrl', { required: true })}
/>
</Flex>
<Flex mt={6}>
<Flex
alignItems={'center'}
flex={['', '0 0 110px']}
color={'myGray.900'}
fontWeight={500}
fontSize={'sm'}
>
Authorization
</Flex>
<Input
bg={'myWhite.600'}
placeholder={t('dataset:request_headers')}
maxLength={200}
{...register('apiServer.authorization')}
/>
</Flex>
</>
)}
{/* @ts-ignore */}
<ApiDatasetForm type={type} form={form} />
</ModalBody>
<ModalFooter px={9}>

View File

@@ -24,6 +24,14 @@ const SideTag = ({ type, ...props }: { type: `${DatasetTypeEnum}` } & FlexProps)
[DatasetTypeEnum.apiDataset]: {
icon: 'core/dataset/externalDatasetOutline',
label: t('dataset:api_file')
},
[DatasetTypeEnum.feishu]: {
icon: 'core/dataset/feishuDatasetOutline',
label: t('dataset:feishu_dataset')
},
[DatasetTypeEnum.yuque]: {
icon: 'core/dataset/yuqueDatasetOutline',
label: t('dataset:yuque_dataset')
}
};
}, [t]);

View File

@@ -64,7 +64,10 @@ const Dataset = () => {
const onSelectDatasetType = useCallback(
(e: CreateDatasetType) => {
if (!feConfigs?.isPlus && e === DatasetTypeEnum.websiteDataset) {
if (
!feConfigs?.isPlus &&
[DatasetTypeEnum.websiteDataset, DatasetTypeEnum.feishu, DatasetTypeEnum.yuque].includes(e)
) {
return toast({
status: 'warning',
title: t('common:common.system.Commercial version function')
@@ -168,6 +171,18 @@ const Dataset = () => {
label: t('dataset:website_dataset'),
description: t('dataset:website_dataset_desc'),
onClick: () => onSelectDatasetType(DatasetTypeEnum.websiteDataset)
},
{
icon: 'core/dataset/feishuDatasetColor',
label: t('dataset:feishu_dataset'),
description: t('dataset:feishu_dataset_desc'),
onClick: () => onSelectDatasetType(DatasetTypeEnum.feishu)
},
{
icon: 'core/dataset/yuqueDatasetColor',
label: t('dataset:yuque_dataset'),
description: t('dataset:yuque_dataset_desc'),
onClick: () => onSelectDatasetType(DatasetTypeEnum.yuque)
}
]
},

View File

@@ -6,7 +6,7 @@ import { OAuthEnum } from '@fastgpt/global/support/user/constant';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { customAlphabet } from 'nanoid';
import { useRouter } from 'next/router';
import { Dispatch, useRef } from 'react';
import { Dispatch, useMemo, useRef } from 'react';
import { useTranslation } from 'next-i18next';
import I18nLngSelector from '@/components/Select/I18nLngSelector';
import { useSystem } from '@fastgpt/web/hooks/useSystem';
@@ -42,6 +42,16 @@ const FormLayout = ({ children, setPageType, pageType }: Props) => {
}
]
: []),
...(feConfigs?.oauth?.dingtalk
? [
{
label: t('user:login.Dingtalk'),
provider: OAuthEnum.dingtalk,
icon: 'common/dingtalkFill',
redirectUrl: `https://login.dingtalk.com/oauth2/auth?client_id=${feConfigs?.oauth?.dingtalk}&redirect_uri=${redirectUri}&state=${state.current}&response_type=code&scope=openid&prompt=consent`
}
]
: []),
...(feConfigs?.oauth?.google
? [
{
@@ -85,8 +95,10 @@ const FormLayout = ({ children, setPageType, pageType }: Props) => {
: [])
];
const show_oauth =
!sessionStorage.getItem('bd_vid') && !!(feConfigs?.sso?.url || oAuthList.length > 0);
const show_oauth = useMemo(
() => !sessionStorage.getItem('bd_vid') && !!(feConfigs?.sso?.url || oAuthList.length > 0),
[feConfigs?.sso?.url, oAuthList.length]
);
return (
<Flex flexDirection={'column'} h={'100%'}>

View File

@@ -45,16 +45,6 @@ const RegisterForm = ({ setPageType, loginSuccess }: Props) => {
const { runAsync: onclickRegister, loading: requesting } = useRequest2(
async ({ username, password, code }: RegisterType) => {
const fastgpt_sem = (() => {
try {
return sessionStorage.getItem('fastgpt_sem')
? JSON.parse(sessionStorage.getItem('fastgpt_sem')!)
: undefined;
} catch {
return undefined;
}
})();
loginSuccess(
await postRegister({
username,
@@ -62,7 +52,16 @@ const RegisterForm = ({ setPageType, loginSuccess }: Props) => {
password,
inviterId: localStorage.getItem('inviterId') || undefined,
bd_vid: sessionStorage.getItem('bd_vid') || undefined,
fastgpt_sem: fastgpt_sem
fastgpt_sem: (() => {
try {
return sessionStorage.getItem('fastgpt_sem')
? JSON.parse(sessionStorage.getItem('fastgpt_sem')!)
: undefined;
} catch {
return undefined;
}
})(),
sourceDomain: sessionStorage.getItem('sourceDomain') || undefined
})
);

View File

@@ -43,7 +43,18 @@ const provider = () => {
type: loginStore?.provider as `${OAuthEnum}`,
code,
callbackUrl: `${location.origin}/login/provider`,
inviterId: localStorage.getItem('inviterId') || undefined
inviterId: localStorage.getItem('inviterId') || undefined,
bd_vid: sessionStorage.getItem('bd_vid') || undefined,
fastgpt_sem: (() => {
try {
return sessionStorage.getItem('fastgpt_sem')
? JSON.parse(sessionStorage.getItem('fastgpt_sem')!)
: undefined;
} catch {
return undefined;
}
})(),
sourceDomain: sessionStorage.getItem('sourceDomain') || undefined
});
if (!res) {
@@ -79,12 +90,8 @@ const provider = () => {
return;
}
if (
!code ||
!loginStore ||
(loginStore.provider !== OAuthEnum.sso && (!loginStore.state || !state))
)
return;
console.log('SSO', { loginStore, code, state });
if (!code || !loginStore) return;
if (isOauthLogging) return;

View File

@@ -0,0 +1,15 @@
import { GetApiDatasetFileListResponse } from '@/pages/api/core/dataset/apiDataset/list';
import { FeishuServer, YuqueServer } from '@fastgpt/global/core/dataset/apiDataset';
import { POST } from '@fastgpt/service/common/api/plusRequest';
export const getFeishuAndYuqueDatasetFileList = async (data: {
feishuServer?: FeishuServer;
yuqueServer?: YuqueServer;
parentId?: string;
}) => {
const res = await POST<GetApiDatasetFileListResponse>('/core/dataset/systemApiDataset', {
type: 'list',
...data
});
return res;
};

View File

@@ -0,0 +1,21 @@
import { POST } from '@/web/common/api/request';
import { TrackEnum } from '@fastgpt/global/common/middle/tracks/constants';
import { useSystemStore } from '../../system/useSystemStore';
const createTrack = ({ event, data }: { event: TrackEnum; data: any }) => {
if (!useSystemStore.getState()?.feConfigs?.isPlus) return;
return POST('/common/tracks/push', {
event,
data
});
};
export const webPushTrack = {
useAppTemplate: (data: { id: string; name: string }) => {
return createTrack({
event: TrackEnum.useAppTemplate,
data
});
}
};

View File

@@ -8,7 +8,12 @@ import { TrackEventName } from '../common/system/constants';
export const useInitApp = () => {
const router = useRouter();
const { hiId, bd_vid, k } = router.query as { hiId?: string; bd_vid?: string; k?: string };
const { hiId, bd_vid, k, sourceDomain } = router.query as {
hiId?: string;
bd_vid?: string;
k?: string;
sourceDomain?: string;
};
const { loadGitStar, setInitd, feConfigs } = useSystemStore();
const [scripts, setScripts] = useState<FastGPTFeConfigsType['scripts']>([]);
const [title, setTitle] = useState(process.env.SYSTEM_NAME || 'AI');
@@ -61,7 +66,16 @@ export const useInitApp = () => {
hiId && localStorage.setItem('inviterId', hiId);
bd_vid && sessionStorage.setItem('bd_vid', bd_vid);
k && sessionStorage.setItem('fastgpt_sem', JSON.stringify({ keyword: k }));
}, [bd_vid, hiId, k]);
const formatSourceDomain = (() => {
if (sourceDomain) return sourceDomain;
return document.referrer;
})();
console.log(formatSourceDomain, '-=-=');
if (formatSourceDomain && !sessionStorage.getItem('sourceDomain')) {
sessionStorage.setItem('sourceDomain', formatSourceDomain);
}
}, [bd_vid, hiId, k, sourceDomain]);
return {
feConfigs,

View File

@@ -1,6 +1,10 @@
import { AppTypeEnum } from '@fastgpt/global/core/app/constants';
import { AppDetailType } from '@fastgpt/global/core/app/type.d';
import type { FeishuAppType, OutLinkEditType } from '@fastgpt/global/support/outLink/type.d';
import type {
DingtalkAppType,
FeishuAppType,
OutLinkEditType
} from '@fastgpt/global/support/outLink/type.d';
import { AppPermission } from '@fastgpt/global/support/permission/app/controller';
export const defaultApp: AppDetailType = {
_id: '',
@@ -39,6 +43,14 @@ export const defaultFeishuOutLinkForm: OutLinkEditType<FeishuAppType> = {
}
};
export const defaultDingtalkOutlinkForm: OutLinkEditType<DingtalkAppType> = {
name: '',
limit: {
QPM: 100,
maxUsagePoints: -1
}
};
export enum TTSTypeEnum {
none = 'none',
web = 'web',

View File

@@ -66,6 +66,7 @@ import type {
listExistIdQuery,
listExistIdResponse
} from '@/pages/api/core/dataset/apiDataset/listExistId';
import { FeishuServer, YuqueServer } from '@fastgpt/global/core/dataset/apiDataset';
/* ======================== dataset ======================= */
export const getDatasets = (data: GetDatasetListBody) =>

View File

@@ -65,3 +65,13 @@ export enum ImportProcessWayEnum {
auto = 'auto',
custom = 'custom'
}
export const datasetTypeCourseMap: Record<`${DatasetTypeEnum}`, string> = {
[DatasetTypeEnum.folder]: '',
[DatasetTypeEnum.dataset]: '',
[DatasetTypeEnum.apiDataset]: '/docs/guide/knowledge_base/api_dataset/',
[DatasetTypeEnum.websiteDataset]: '/docs/guide/knowledge_base/websync/',
[DatasetTypeEnum.feishu]: '/docs/guide/knowledge_base/lark_dataset/',
[DatasetTypeEnum.yuque]: '/docs/guide/knowledge_base/yuque_dataset/',
[DatasetTypeEnum.externalFile]: ''
};

View File

@@ -121,6 +121,19 @@ export const DatasetPageContextProvider = ({
baseUrl: data.apiServer.baseUrl,
authorization: ''
}
: undefined,
yuqueServer: data.yuqueServer
? {
userId: data.yuqueServer.userId,
token: ''
}
: undefined,
feishuServer: data.feishuServer
? {
appId: data.feishuServer.appId,
appSecret: '',
folderToken: data.feishuServer.folderToken
}
: undefined
}));
}

View File

@@ -1,3 +1,6 @@
.react-flow {
overflow: visible;
}
.react-flow__panel.react-flow__attribution {
z-index: 0;
left: 0;

View File

@@ -9,7 +9,10 @@ import type {
OauthLoginProps,
PostLoginProps
} from '@fastgpt/global/support/user/api.d';
import { GetWXLoginQRResponse } from '@fastgpt/global/support/user/login/api.d';
import {
AccountRegisterBody,
GetWXLoginQRResponse
} from '@fastgpt/global/support/user/login/api.d';
export const sendAuthCode = (data: {
username: string;
@@ -33,16 +36,7 @@ export const postRegister = ({
inviterId,
bd_vid,
fastgpt_sem
}: {
username: string;
code: string;
password: string;
inviterId?: string;
bd_vid?: string;
fastgpt_sem?: {
keyword: string;
};
}) =>
}: AccountRegisterBody) =>
POST<ResLogin>(`/proApi/support/user/account/register/emailAndPhone`, {
username,
code,