diff --git a/.vscode/nextapi.code-snippets b/.vscode/nextapi.code-snippets index 088ed3b3f8e3..8f7457743a50 100644 --- a/.vscode/nextapi.code-snippets +++ b/.vscode/nextapi.code-snippets @@ -35,17 +35,19 @@ "scope": "typescriptreact", "prefix": "context", "body": [ - "import { ReactNode } from 'react';", + "import React, { ReactNode } from 'react';", "import { createContext } from 'use-context-selector';", "", "type ContextType = {$1};", "", "export const Context = createContext({});", "", - "export const ContextProvider = ({ children }: { children: ReactNode }) => {", + "const ContextProvider = ({ children }: { children: ReactNode }) => {", " const contextValue: ContextType = {};", " return {children};", "};", + "", + "export default ContextProvider" ], "description": "FastGPT usecontext template" } diff --git a/docSite/content/zh-cn/docs/development/upgrading/484.md b/docSite/content/zh-cn/docs/development/upgrading/484.md index fd551cd42313..d673a2da32c6 100644 --- a/docSite/content/zh-cn/docs/development/upgrading/484.md +++ b/docSite/content/zh-cn/docs/development/upgrading/484.md @@ -16,7 +16,9 @@ weight: 821 ## V4.8.4 更新说明 1. 新增 - 应用使用新权限系统。 -2. 优化 - 文本分割增加连续换行、制表符清除,避免大文本性能问题。 -3. 修复 - Debug 模式下,相同 source 和 target 内容,导致连线显示异常。 -4. 修复 - 定时执行初始化错误。 -5. 调整组件库全局theme。 \ No newline at end of file +2. 新增 - 应用支持文件夹。 +3. 优化 - 文本分割增加连续换行、制表符清除,避免大文本性能问题。 +4. 修复 - Debug 模式下,相同 source 和 target 内容,导致连线显示异常。 +5. 修复 - 定时执行初始化错误。 +6. 修复 - 应用调用传参异常。 +7. 调整组件库全局theme。 \ No newline at end of file diff --git a/packages/global/common/parentFolder/type.d.ts b/packages/global/common/parentFolder/type.d.ts index c394959ae37d..800b6834add0 100644 --- a/packages/global/common/parentFolder/type.d.ts +++ b/packages/global/common/parentFolder/type.d.ts @@ -2,3 +2,17 @@ export type ParentTreePathItemType = { parentId: string; parentName: string; }; +export type ParentIdType = string | null | undefined; + +export type GetResourceFolderListProps = { + parentId: ParentIdType; +}; +export type GetResourceFolderListItemResponse = { + name: string; + id: string; +}; + +export type GetResourceListItemResponse = GetResourceFolderListItemResponse & { + avatar: string; + isFolder: boolean; +}; diff --git a/packages/global/common/parentFolder/utils.ts b/packages/global/common/parentFolder/utils.ts new file mode 100644 index 000000000000..dfd8b2d18d4a --- /dev/null +++ b/packages/global/common/parentFolder/utils.ts @@ -0,0 +1,17 @@ +import { ParentIdType } from './type'; + +export const parseParentIdInMongo = (parentId: ParentIdType) => { + if (parentId === undefined) return {}; + + if (parentId === null || parentId === '') + return { + parentId: null + }; + + const pattern = /^[0-9a-fA-F]{24}$/; + if (pattern.test(parentId)) + return { + parentId + }; + return {}; +}; diff --git a/packages/global/core/app/constants.ts b/packages/global/core/app/constants.ts index 136d0439dac9..4ad12b2a4014 100644 --- a/packages/global/core/app/constants.ts +++ b/packages/global/core/app/constants.ts @@ -1,10 +1,14 @@ import { AppTTSConfigType, AppWhisperConfigType } from './type'; export enum AppTypeEnum { + folder = 'folder', simple = 'simple', advanced = 'advanced' } export const AppTypeMap = { + [AppTypeEnum.folder]: { + label: 'folder' + }, [AppTypeEnum.simple]: { label: 'simple' }, diff --git a/packages/global/core/app/type.d.ts b/packages/global/core/app/type.d.ts index f297a2f65f97..19185659a82b 100644 --- a/packages/global/core/app/type.d.ts +++ b/packages/global/core/app/type.d.ts @@ -1,5 +1,4 @@ import type { FlowNodeTemplateType, StoreNodeItemType } from '../workflow/type'; - import { AppTypeEnum } from './constants'; import { PermissionTypeEnum } from '../../support/permission/constant'; import { VariableInputEnum } from '../workflow/constants'; @@ -9,14 +8,17 @@ import { TeamTagSchema as TeamTagsSchemaType } from '@fastgpt/global/support/use import { StoreEdgeItemType } from '../workflow/type/edge'; import { PermissionValueType } from '../../support/permission/type'; import { AppPermission } from '../../support/permission/app/controller'; +import { ParentIdType } from '../../common/parentFolder/type'; export type AppSchema = { _id: string; + parentId?: ParentIdType; teamId: string; tmbId: string; - name: string; - type: `${AppTypeEnum}`; + type: AppTypeEnum; version?: 'v1' | 'v2'; + + name: string; avatar: string; intro: string; updateTime: number; @@ -39,6 +41,7 @@ export type AppListItemType = { name: string; avatar: string; intro: string; + type: AppTypeEnum; defaultPermission: PermissionValueType; permission: AppPermission; }; diff --git a/packages/global/core/app/utils.ts b/packages/global/core/app/utils.ts index fc4dcbc1da4d..667ee89d80ee 100644 --- a/packages/global/core/app/utils.ts +++ b/packages/global/core/app/utils.ts @@ -21,7 +21,7 @@ export const getDefaultAppForm = (): AppSimpleEditFormType => ({ limit: 1500, searchMode: DatasetSearchModeEnum.embedding, usingReRank: false, - datasetSearchUsingExtensionQuery: true, + datasetSearchUsingExtensionQuery: false, datasetSearchExtensionBg: '' }, selectedTools: [], diff --git a/packages/global/core/workflow/template/system/runApp.ts b/packages/global/core/workflow/template/system/runApp.ts index 425b609b2cd0..7d006b108b77 100644 --- a/packages/global/core/workflow/template/system/runApp.ts +++ b/packages/global/core/workflow/template/system/runApp.ts @@ -35,7 +35,10 @@ export const RunAppModule: FlowNodeTemplateType = { required: true }, Input_Template_History, - Input_Template_UserChatInput + { + ...Input_Template_UserChatInput, + toolDescription: '用户问题' + } ], outputs: [ { diff --git a/packages/global/core/workflow/type/index.d.ts b/packages/global/core/workflow/type/index.d.ts index 04409abe7c64..726fec06d49c 100644 --- a/packages/global/core/workflow/type/index.d.ts +++ b/packages/global/core/workflow/type/index.d.ts @@ -105,8 +105,8 @@ export type NodeSourceNodeItemType = { /* --------------- function type -------------------- */ export type SelectAppItemType = { id: string; - name: string; - logo: string; + // name: string; + // logo?: string; }; /* agent */ diff --git a/packages/global/support/permission/app/constant.ts b/packages/global/support/permission/app/constant.ts index a9479ccbc8b6..72cc4980557d 100644 --- a/packages/global/support/permission/app/constant.ts +++ b/packages/global/support/permission/app/constant.ts @@ -17,4 +17,4 @@ export const AppPermissionList: PermissionListType = { } }; -export const AppDefaultPermission = NullPermission; +export const AppDefaultPermissionVal = NullPermission; diff --git a/packages/global/support/permission/app/controller.ts b/packages/global/support/permission/app/controller.ts index 90cf507d75cc..09a880f6fb7a 100644 --- a/packages/global/support/permission/app/controller.ts +++ b/packages/global/support/permission/app/controller.ts @@ -1,14 +1,14 @@ import { PerConstructPros, Permission } from '../controller'; -import { AppDefaultPermission } from './constant'; +import { AppDefaultPermissionVal } from './constant'; export class AppPermission extends Permission { constructor(props?: PerConstructPros) { if (!props) { props = { - per: AppDefaultPermission + per: AppDefaultPermissionVal }; } else if (!props?.per) { - props.per = AppDefaultPermission; + props.per = AppDefaultPermissionVal; } super(props); } diff --git a/packages/global/support/permission/collaborator.d.ts b/packages/global/support/permission/collaborator.d.ts index 8a3dcff31969..9225151d1f97 100644 --- a/packages/global/support/permission/collaborator.d.ts +++ b/packages/global/support/permission/collaborator.d.ts @@ -1,9 +1,10 @@ +import { Permission } from './controller'; import { PermissionValueType } from './type'; export type CollaboratorItemType = { teamId: string; tmbId: string; - permission: PermissionValueType; + permission: Permission; name: string; avatar: string; }; diff --git a/packages/global/support/permission/user/constant.ts b/packages/global/support/permission/user/constant.ts index 001d8ebe962f..8dcb8a81e357 100644 --- a/packages/global/support/permission/user/constant.ts +++ b/packages/global/support/permission/user/constant.ts @@ -9,7 +9,7 @@ export const TeamPermissionList = { }, [PermissionKeyEnum.manage]: { ...PermissionList[PermissionKeyEnum.manage], - description: '可邀请, 删除成员' + description: '可创建资源、邀请、删除成员' } }; diff --git a/packages/service/core/app/controller.ts b/packages/service/core/app/controller.ts index dd45fad32c0d..68b2e7bb7c69 100644 --- a/packages/service/core/app/controller.ts +++ b/packages/service/core/app/controller.ts @@ -3,6 +3,7 @@ import { NodeInputKeyEnum } from '@fastgpt/global/core/workflow/constants'; import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant'; import { getLLMModel } from '../ai/model'; import { MongoAppVersion } from './version/schema'; +import { MongoApp } from './schema'; export const beforeUpdateAppFormat = ({ nodes @@ -65,3 +66,40 @@ export const getAppLatestVersion = async (appId: string, app?: AppSchema) => { chatConfig: app?.chatConfig || {} }; }; + +/* Get apps */ +export async function findAppAndAllChildren({ + teamId, + appId, + fields +}: { + teamId: string; + appId: string; + fields?: string; +}): Promise { + const find = async (id: string) => { + const children = await MongoApp.find( + { + teamId, + parentId: id + }, + fields + ).lean(); + + let apps = children; + + for (const child of children) { + const grandChildrenIds = await find(child._id); + apps = apps.concat(grandChildrenIds); + } + + return apps; + }; + const [app, childDatasets] = await Promise.all([MongoApp.findById(appId, fields), find(appId)]); + + if (!app) { + return Promise.reject('Dataset not found'); + } + + return [app, ...childDatasets]; +} diff --git a/packages/service/core/app/schema.ts b/packages/service/core/app/schema.ts index f3e81b1f98f3..fdc87af96ecf 100644 --- a/packages/service/core/app/schema.ts +++ b/packages/service/core/app/schema.ts @@ -1,4 +1,4 @@ -import { AppTypeMap } from '@fastgpt/global/core/app/constants'; +import { AppTypeEnum, AppTypeMap } from '@fastgpt/global/core/app/constants'; import { connectionMongo, type Model } from '../../common/mongo'; const { Schema, model, models } = connectionMongo; import type { AppSchema as AppType } from '@fastgpt/global/core/app/type.d'; @@ -7,7 +7,7 @@ import { TeamCollectionName, TeamMemberCollectionName } from '@fastgpt/global/support/user/team/constant'; -import { AppDefaultPermission } from '@fastgpt/global/support/permission/app/constant'; +import { AppDefaultPermissionVal } from '@fastgpt/global/support/permission/app/constant'; export const AppCollectionName = 'apps'; @@ -22,6 +22,11 @@ export const chatConfigType = { }; const AppSchema = new Schema({ + parentId: { + type: Schema.Types.ObjectId, + ref: AppCollectionName, + default: null + }, teamId: { type: Schema.Types.ObjectId, ref: TeamCollectionName, @@ -38,8 +43,8 @@ const AppSchema = new Schema({ }, type: { type: String, - default: 'advanced', - enum: Object.keys(AppTypeMap) + default: AppTypeEnum.advanced, + enum: Object.values(AppTypeEnum) }, version: { type: String, @@ -104,13 +109,13 @@ const AppSchema = new Schema({ // the default permission of a app defaultPermission: { type: Number, - default: AppDefaultPermission + default: AppDefaultPermissionVal } }); try { AppSchema.index({ updateTime: -1 }); - AppSchema.index({ teamId: 1 }); + AppSchema.index({ teamId: 1, type: 1 }); AppSchema.index({ scheduledTriggerConfig: 1, intervalNextTime: -1 }); } catch (error) { console.log(error); diff --git a/packages/service/core/workflow/dispatch/tools/runApp.ts b/packages/service/core/workflow/dispatch/tools/runApp.ts index df1de2098840..96f7351bb31b 100644 --- a/packages/service/core/workflow/dispatch/tools/runApp.ts +++ b/packages/service/core/workflow/dispatch/tools/runApp.ts @@ -17,6 +17,8 @@ import { DispatchNodeResponseKeyEnum } from '@fastgpt/global/core/workflow/runti import { getHistories } from '../utils'; import { chatValue2RuntimePrompt, runtimePrompt2ChatsValue } from '@fastgpt/global/core/chat/adapt'; import { DispatchNodeResultType } from '@fastgpt/global/core/workflow/runtime/type'; +import { authAppByTmbId } from '../../../../support/permission/app/auth'; +import { ReadPermissionVal } from '@fastgpt/global/support/permission/constant'; type Props = ModuleDispatchProps<{ [NodeInputKeyEnum.userChatInput]: string; @@ -32,27 +34,25 @@ export const dispatchAppRequest = async (props: Props): Promise => { const { res, teamId, + tmbId, stream, detail, histories, query, params: { userChatInput, history, app } } = props; - let start = Date.now(); if (!userChatInput) { return Promise.reject('Input is empty'); } - const appData = await MongoApp.findOne({ - _id: app.id, - teamId + const { app: appData } = await authAppByTmbId({ + appId: app.id, + teamId, + tmbId, + per: ReadPermissionVal }); - if (!appData) { - return Promise.reject('App not found'); - } - if (res && stream) { responseWrite({ res, @@ -64,6 +64,7 @@ export const dispatchAppRequest = async (props: Props): Promise => { } const chatHistories = getHistories(history, histories); + const { files } = chatValue2RuntimePrompt(query); const { flowResponses, flowUsages, assistantResponses } = await dispatchWorkFlow({ ...props, @@ -71,11 +72,11 @@ export const dispatchAppRequest = async (props: Props): Promise => { runtimeNodes: storeNodes2RuntimeNodes(appData.modules, getDefaultEntryNodeIds(appData.modules)), runtimeEdges: initWorkflowEdgeStatus(appData.edges), histories: chatHistories, - query, - variables: { - ...props.variables, - userChatInput - } + query: runtimePrompt2ChatsValue({ + files, + text: userChatInput + }), + variables: props.variables }); const completeMessages = chatHistories.concat([ diff --git a/packages/service/support/permission/teamLimit.ts b/packages/service/support/permission/teamLimit.ts index d75b61c8e040..d84e60ddedb7 100644 --- a/packages/service/support/permission/teamLimit.ts +++ b/packages/service/support/permission/teamLimit.ts @@ -5,6 +5,7 @@ import { MongoDataset } from '../../core/dataset/schema'; import { DatasetTypeEnum } from '@fastgpt/global/core/dataset/constants'; import { TeamErrEnum } from '@fastgpt/global/common/error/code/team'; import { SystemErrEnum } from '@fastgpt/global/common/error/code/system'; +import { AppTypeEnum } from '@fastgpt/global/core/app/constants'; export const checkDatasetLimit = async ({ teamId, @@ -66,7 +67,7 @@ export const checkTeamDatasetLimit = async (teamId: string) => { export const checkTeamAppLimit = async (teamId: string) => { const [{ standardConstants }, appCount] = await Promise.all([ getTeamStandPlan({ teamId }), - MongoApp.count({ teamId }) + MongoApp.count({ teamId, type: { $in: [AppTypeEnum.advanced, AppTypeEnum.simple] } }) ]); if (standardConstants && appCount >= standardConstants.maxAppAmount) { diff --git a/packages/web/components/common/Icon/constants.ts b/packages/web/components/common/Icon/constants.ts index 4cbd370c26cd..2f7a09872bd2 100644 --- a/packages/web/components/common/Icon/constants.ts +++ b/packages/web/components/common/Icon/constants.ts @@ -46,6 +46,7 @@ export const iconPaths = { 'common/refreshLight': () => import('./icons/common/refreshLight.svg'), 'common/resultLight': () => import('./icons/common/resultLight.svg'), 'common/retryLight': () => import('./icons/common/retryLight.svg'), + 'common/rightArrowFill': () => import('./icons/common/rightArrowFill.svg'), 'common/rightArrowLight': () => import('./icons/common/rightArrowLight.svg'), 'common/routePushLight': () => import('./icons/common/routePushLight.svg'), 'common/saveFill': () => import('./icons/common/saveFill.svg'), @@ -71,6 +72,7 @@ export const iconPaths = { 'core/app/publish/lark': () => import('./icons/core/app/publish/lark.svg'), 'core/app/questionGuide': () => import('./icons/core/app/questionGuide.svg'), 'core/app/schedulePlan': () => import('./icons/core/app/schedulePlan.svg'), + 'core/app/simpleBot': () => import('./icons/core/app/simpleBot.svg'), 'core/app/simpleMode/ai': () => import('./icons/core/app/simpleMode/ai.svg'), 'core/app/simpleMode/chat': () => import('./icons/core/app/simpleMode/chat.svg'), 'core/app/simpleMode/dataset': () => import('./icons/core/app/simpleMode/dataset.svg'), diff --git a/packages/web/components/common/Icon/icons/common/loading.svg b/packages/web/components/common/Icon/icons/common/loading.svg index 7139e52c8499..833300eca893 100644 --- a/packages/web/components/common/Icon/icons/common/loading.svg +++ b/packages/web/components/common/Icon/icons/common/loading.svg @@ -1,52 +1,78 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/packages/web/components/common/Icon/icons/common/rightArrowFill.svg b/packages/web/components/common/Icon/icons/common/rightArrowFill.svg new file mode 100644 index 000000000000..29d44db5c124 --- /dev/null +++ b/packages/web/components/common/Icon/icons/common/rightArrowFill.svg @@ -0,0 +1,4 @@ + + + \ No newline at end of file diff --git a/packages/web/components/common/Icon/icons/core/app/simpleBot.svg b/packages/web/components/common/Icon/icons/core/app/simpleBot.svg new file mode 100644 index 000000000000..1a0f2e0b3656 --- /dev/null +++ b/packages/web/components/common/Icon/icons/core/app/simpleBot.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/packages/web/components/common/MyDivider/index.tsx b/packages/web/components/common/MyDivider/index.tsx index 42a68617c445..1b0491cddccf 100644 --- a/packages/web/components/common/MyDivider/index.tsx +++ b/packages/web/components/common/MyDivider/index.tsx @@ -3,7 +3,14 @@ import { Divider, type DividerProps } from '@chakra-ui/react'; const MyDivider = (props: DividerProps) => { const { h } = props; - return ; + return ( + + ); }; export default MyDivider; diff --git a/packages/web/components/common/MyLoading/index.tsx b/packages/web/components/common/MyLoading/index.tsx index 4bc41e253f43..9303b6482675 100644 --- a/packages/web/components/common/MyLoading/index.tsx +++ b/packages/web/components/common/MyLoading/index.tsx @@ -6,7 +6,7 @@ const Loading = ({ text = '', bg = 'rgba(255,255,255,0.5)', zIndex = 1000, - size = 'xl' + size = 'lg' }: { fixed?: boolean; text?: string; @@ -17,7 +17,7 @@ const Loading = ({ return ( any; }[]; }[]; @@ -33,7 +36,8 @@ export type Props = { const MyMenu = ({ width = 'auto', trigger = 'hover', - offset = [0, 5], + offset, + iconSize = '1rem', Button, menuList }: Props) => { @@ -45,8 +49,8 @@ const MyMenu = ({ } }, danger: { + color: 'red.600', _hover: { - color: 'red.600', background: 'red.1' } } @@ -70,8 +74,14 @@ const MyMenu = ({ } }); + const computeOffset = useMemo<[number, number]>(() => { + if (offset) return offset; + if (typeof width === 'number') return [-width / 2, 5]; + return [0, 5]; + }, [offset]); + return ( - + { @@ -90,7 +100,8 @@ const MyMenu = ({ > { + onClickCapture={(e) => { + e.stopPropagation(); if (trigger === 'click') { setIsOpen(!isOpen); } @@ -124,8 +135,7 @@ const MyMenu = ({ { + onClickCapture={(e) => { e.stopPropagation(); setIsOpen(false); child.onClick && child.onClick(); @@ -133,9 +143,19 @@ const MyMenu = ({ color={child.isActive ? 'primary.700' : 'myGray.600'} whiteSpace={'pre-wrap'} _notLast={{ mb: 0.5 }} + {...typeMapStyle[child.type || 'primary']} > - {!!child.icon && } - {child.label} + {!!child.icon && } + + + {child.label} + + {child.description && ( + + {child.description} + + )} + ))} diff --git a/packages/web/components/common/MyModal/EditFolderModal.tsx b/packages/web/components/common/MyModal/EditFolderModal.tsx new file mode 100644 index 000000000000..8b461ace6da9 --- /dev/null +++ b/packages/web/components/common/MyModal/EditFolderModal.tsx @@ -0,0 +1,92 @@ +import React, { useMemo } from 'react'; +import { ModalFooter, ModalBody, Input, Button, Box, Textarea } from '@chakra-ui/react'; +import MyModal from './index'; +import { useTranslation } from 'next-i18next'; +import { useRequest2 } from '../../../hooks/useRequest'; +import FormLabel from '../MyBox/FormLabel'; +import { useForm } from 'react-hook-form'; + +export type EditFolderFormType = { + id?: string; + name?: string; + intro?: string; +}; +type CommitType = { + name: string; + intro?: string; +}; + +const EditFolderModal = ({ + onClose, + onCreate, + onEdit, + id, + name, + intro +}: EditFolderFormType & { + onClose: () => void; + onCreate: (data: CommitType) => any; + onEdit: (data: CommitType & { id: string }) => any; +}) => { + const { t } = useTranslation(); + const isEdit = !!id; + const { register, handleSubmit } = useForm({ + defaultValues: { + name, + intro + } + }); + + const typeMap = useMemo( + () => + isEdit + ? { + title: t('dataset.Edit Folder') + } + : { + title: t('dataset.Create Folder') + }, + [isEdit, t] + ); + + const { run: onSave, loading } = useRequest2( + ({ name = '', intro }: EditFolderFormType) => { + if (!name) return; + + if (isEdit) return onEdit({ id, name, intro }); + return onCreate({ name, intro }); + }, + { + onSuccess: (res) => { + onClose(); + } + } + ); + + return ( + + + + {t('common.Input name')} + + + + {t('common.Input folder description')} +