Compare commits

...

6 Commits

Author SHA1 Message Date
Archer
c722ced68d 4.8.12 test (#2994)
* perf: run loop code

* doc

* fix: mulity loop node will error; loop node variables cannot inherit

* back save tip position

* fix: child workflow runtime

* stream connection
2024-10-25 23:13:53 +08:00
Archer
f89452acdd Group role (#2993)
* feat: app/dataset support group (#2898)

* pref: member-group (#2862)

* feat: group list ordered by updateTime

* fix: transfer ownership of group when deleting member

* fix: i18n fix

* feat: can not set member as admin/owner when user is not active

* fix: GroupInfoModal hover input do not change color

* fix(fe): searchinput do not scroll

* feat: app collaborator with group, remove default permission

* feat: dataset collaborator with group, remove default permission

* chore(test): pref mock

* chore: remove useless code

* chore: adjust

* fix: add self as collaborator when creating folder

* fix(fe): folder manage menu do not show when user has write permission
only

* fix: dataset folder create

* feat: Add code comment

* Pref: app move (#2952)

* perf: app schema

* doc

---------

Co-authored-by: Finley Ge <32237950+FinleyGe@users.noreply.github.com>
2024-10-25 19:39:11 +08:00
Archer
74d58d562b 4.8.12 test fix (#2988)
* perf: qps limit

* perf: http response data

* perf: json path check

* fix: ts

* loop support reference parent variable
2024-10-25 16:34:26 +08:00
heheer
165fe077bc fix: http raw response undefined (#2981) 2024-10-25 11:53:32 +08:00
Finley Ge
75494f8d01 feat: QPS Limit middleware (#2956)
* feat: QPS Limit middleware

* chore: use request-ip to get client ip

* feat: frequencyLimit schema
2024-10-25 10:08:59 +08:00
papapatrick
bb727b0710 add bing search plugins (#2970) 2024-10-23 22:45:06 +08:00
87 changed files with 1997 additions and 1249 deletions

View File

@@ -9,6 +9,26 @@ weight: 812
## 更新指南 ## 更新指南
### 1. 做好数据备份
### 2. 修改镜像
- 更新 FastGPT 镜像 tag: v4.8.12-beta
- 更新 FastGPT 商业版镜像 tag: v4.8.12-beta
- Sandbox 镜像,可以不更新
### 3. 商业版执行初始化
从任意终端,发起 1 个 HTTP 请求。其中 {{rootkey}} 替换成环境变量里的 `rootkey`{{host}} 替换成**FastGPT 商业版域名**。
```bash
curl --location --request POST 'https://{{host}}/api/admin/init/4812' \
--header 'rootkey: {{rootkey}}' \
--header 'Content-Type: application/json'
```
会初始化应用和知识库的成员组数据。
## 更新说明 ## 更新说明
@@ -23,7 +43,12 @@ weight: 812
9. 新增 - 数据库连接和操作插件 9. 新增 - 数据库连接和操作插件
10. 新增 - Cookie 隐私协议提示 10. 新增 - Cookie 隐私协议提示
11. 新增 - HTTP 节点支持 JSONPath 表达式 11. 新增 - HTTP 节点支持 JSONPath 表达式
12. 修复 - 文件后缀判断,去除 query 影响。 12. 新增 - 应用和知识库支持成员组配置权限
13. 修复 - AI 响应为空时,会造成 LLM 历史记录合并。 13. 优化 - 循环节点支持选择外部节点的变量
14. 修复 - 用户交互节点未阻塞流程 14. 修复 - 文件后缀判断,去除 query 影响
15. 修复 - 新建 APP有时候会导致空指针报错。 15. 修复 - AI 响应为空时,会造成 LLM 历史记录合并。
16. 修复 - 用户交互节点未阻塞流程。
17. 修复 - 新建 APP有时候会导致空指针报错。
18. 修复 - 拥有多个循环节点时,错误运行。
19. 修复 - 循环节点中修改变量,无法传递。
20. 修复 - 非 stream 模式,嵌套子应用/插件执行时无法获取子应用响应。

View File

@@ -19,6 +19,7 @@ export const ERROR_CODE: { [key: number]: string } = {
406: i18nT('common:code_error.error_code.406'), 406: i18nT('common:code_error.error_code.406'),
410: i18nT('common:code_error.error_code.410'), 410: i18nT('common:code_error.error_code.410'),
422: i18nT('common:code_error.error_code.422'), 422: i18nT('common:code_error.error_code.422'),
429: i18nT('common:code_error.error_code.429'),
500: i18nT('common:code_error.error_code.500'), 500: i18nT('common:code_error.error_code.500'),
502: i18nT('common:code_error.error_code.502'), 502: i18nT('common:code_error.error_code.502'),
503: i18nT('common:code_error.error_code.503'), 503: i18nT('common:code_error.error_code.503'),
@@ -39,7 +40,8 @@ export enum ERROR_ENUM {
insufficientQuota = 'insufficientQuota', insufficientQuota = 'insufficientQuota',
unAuthModel = 'unAuthModel', unAuthModel = 'unAuthModel',
unAuthApiKey = 'unAuthApiKey', unAuthApiKey = 'unAuthApiKey',
unAuthFile = 'unAuthFile' unAuthFile = 'unAuthFile',
tooManyRequest = 'tooManyRequest'
} }
export type ErrType<T> = Record< export type ErrType<T> = Record<
@@ -67,6 +69,12 @@ export const ERROR_RESPONSE: Record<
message: i18nT('common:code_error.error_message.403'), message: i18nT('common:code_error.error_message.403'),
data: null data: null
}, },
[ERROR_ENUM.tooManyRequest]: {
code: 429,
statusText: ERROR_ENUM.tooManyRequest,
message: 'Too many request',
data: null
},
[ERROR_ENUM.insufficientQuota]: { [ERROR_ENUM.insufficientQuota]: {
code: 510, code: 510,
statusText: ERROR_ENUM.insufficientQuota, statusText: ERROR_ENUM.insufficientQuota,

View File

@@ -1,4 +1,8 @@
import { UpdateClbPermissionProps } from '../../support/permission/collaborator'; import { RequireOnlyOne } from '../../common/type/utils';
import {
UpdateClbPermissionProps,
UpdatePermissionBody
} from '../../support/permission/collaborator';
import { PermissionValueType } from '../../support/permission/type'; import { PermissionValueType } from '../../support/permission/type';
export type UpdateAppCollaboratorBody = UpdateClbPermissionProps & { export type UpdateAppCollaboratorBody = UpdateClbPermissionProps & {
@@ -7,5 +11,7 @@ export type UpdateAppCollaboratorBody = UpdateClbPermissionProps & {
export type AppCollaboratorDeleteParams = { export type AppCollaboratorDeleteParams = {
appId: string; appId: string;
} & RequireOnlyOne<{
tmbId: string; tmbId: string;
}; groupId: string;
}>;

View File

@@ -10,7 +10,6 @@ import { SelectedDatasetType } from '../workflow/api';
import { DatasetSearchModeEnum } from '../dataset/constants'; import { DatasetSearchModeEnum } from '../dataset/constants';
import { TeamTagSchema as TeamTagsSchemaType } from '@fastgpt/global/support/user/team/type.d'; import { TeamTagSchema as TeamTagsSchemaType } from '@fastgpt/global/support/user/team/type.d';
import { StoreEdgeItemType } from '../workflow/type/edge'; import { StoreEdgeItemType } from '../workflow/type/edge';
import { PermissionSchemaType, PermissionValueType } from '../../support/permission/type';
import { AppPermission } from '../../support/permission/app/controller'; import { AppPermission } from '../../support/permission/app/controller';
import { ParentIdType } from '../../common/parentFolder/type'; import { ParentIdType } from '../../common/parentFolder/type';
import { FlowNodeInputTypeEnum } from 'core/workflow/node/constant'; import { FlowNodeInputTypeEnum } from 'core/workflow/node/constant';
@@ -45,7 +44,11 @@ export type AppSchema = {
inited?: boolean; inited?: boolean;
teamTags: string[]; teamTags: string[];
} & PermissionSchemaType; inheritPermission?: boolean;
// abandon
defaultPermission?: number;
};
export type AppListItemType = { export type AppListItemType = {
_id: string; _id: string;
@@ -57,7 +60,9 @@ export type AppListItemType = {
updateTime: Date; updateTime: Date;
pluginData?: AppSchema['pluginData']; pluginData?: AppSchema['pluginData'];
permission: AppPermission; permission: AppPermission;
} & PermissionSchemaType; inheritPermission?: boolean;
private?: boolean;
};
export type AppDetailType = AppSchema & { export type AppDetailType = AppSchema & {
permission: AppPermission; permission: AppPermission;

View File

@@ -1,5 +1,6 @@
import { UpdateClbPermissionProps } from '../../support/permission/collaborator'; import { UpdateClbPermissionProps } from '../../support/permission/collaborator';
import { PermissionValueType } from '../../support/permission/type'; import { PermissionValueType } from '../../support/permission/type';
import { RequireOnlyOne } from '../../common/type/utils';
export type UpdateDatasetCollaboratorBody = UpdateClbPermissionProps & { export type UpdateDatasetCollaboratorBody = UpdateClbPermissionProps & {
datasetId: string; datasetId: string;
@@ -7,5 +8,7 @@ export type UpdateDatasetCollaboratorBody = UpdateClbPermissionProps & {
export type DatasetCollaboratorDeleteParams = { export type DatasetCollaboratorDeleteParams = {
datasetId: string; datasetId: string;
} & RequireOnlyOne<{
tmbId: string; tmbId: string;
}; groupId: string;
}>;

View File

@@ -1,4 +1,3 @@
import { PermissionSchemaType } from '../../support/permission/type';
import type { LLMModelItemType, VectorModelItemType } from '../../core/ai/model.d'; import type { LLMModelItemType, VectorModelItemType } from '../../core/ai/model.d';
import { PermissionTypeEnum } from '../../support/permission/constant'; import { PermissionTypeEnum } from '../../support/permission/constant';
import { PushDatasetDataChunkProps } from './api'; import { PushDatasetDataChunkProps } from './api';
@@ -32,8 +31,11 @@ export type DatasetSchemaType = {
selector: string; selector: string;
}; };
externalReadUrl?: string; externalReadUrl?: string;
} & PermissionSchemaType; inheritPermission: boolean;
// } & PermissionSchemaType;
// abandon
defaultPermission?: number;
};
export type DatasetCollectionSchemaType = { export type DatasetCollectionSchemaType = {
_id: string; _id: string;
@@ -146,7 +148,9 @@ export type DatasetListItemType = {
type: `${DatasetTypeEnum}`; type: `${DatasetTypeEnum}`;
permission: DatasetPermission; permission: DatasetPermission;
vectorModel: VectorModelItemType; vectorModel: VectorModelItemType;
} & PermissionSchemaType; inheritPermission: boolean;
private?: boolean;
};
export type DatasetItemType = Omit<DatasetSchemaType, 'vectorModel' | 'agentModel'> & { export type DatasetItemType = Omit<DatasetSchemaType, 'vectorModel' | 'agentModel'> & {
vectorModel: VectorModelItemType; vectorModel: VectorModelItemType;

View File

@@ -18,7 +18,7 @@ export const VariableUpdateNode: FlowNodeTemplateType = {
name: i18nT('workflow:variable_update'), name: i18nT('workflow:variable_update'),
intro: i18nT('workflow:update_specified_node_output_or_global_variable'), intro: i18nT('workflow:update_specified_node_output_or_global_variable'),
showStatus: false, showStatus: false,
isTool: false, isTool: true,
version: '481', version: '481',
inputs: [ inputs: [
{ {

View File

@@ -4,11 +4,13 @@ import { PermissionValueType } from './type';
export type CollaboratorItemType = { export type CollaboratorItemType = {
teamId: string; teamId: string;
tmbId: string;
permission: Permission; permission: Permission;
name: string; name: string;
avatar: string; avatar: string;
}; } & RequireOnlyOne<{
tmbId: string;
groupId: string;
}>;
export type UpdateClbPermissionProps = { export type UpdateClbPermissionProps = {
members?: string[]; members?: string[];

View File

@@ -1,4 +1,3 @@
import { Permission } from './controller';
import { PermissionListType } from './type'; import { PermissionListType } from './type';
import { i18nT } from '../../../web/i18n/utils'; import { i18nT } from '../../../web/i18n/utils';
export enum AuthUserTypeEnum { export enum AuthUserTypeEnum {

View File

@@ -1,6 +1,7 @@
import { RequireOnlyOne } from '../../common/type/utils'; import { RequireOnlyOne } from '../../common/type/utils';
import { TeamMemberWithUserSchema } from '../user/team/type'; import { TeamMemberWithUserSchema } from '../user/team/type';
import { AuthUserTypeEnum, PermissionKeyEnum, PerResourceTypeEnum } from './constant'; import { AuthUserTypeEnum, PermissionKeyEnum, PerResourceTypeEnum } from './constant';
import { MemberGroupSchemaType } from './memberGroup/type';
// PermissionValueType, the type of permission's value is a number, which is a bit field actually. // PermissionValueType, the type of permission's value is a number, which is a bit field actually.
// It is spired by the permission system in Linux. // It is spired by the permission system in Linux.
@@ -33,6 +34,10 @@ export type ResourcePerWithTmbWithUser = Omit<ResourcePermissionType, 'tmbId'> &
tmbId: TeamMemberWithUserSchema; tmbId: TeamMemberWithUserSchema;
}; };
export type ResourcePerWithGroup = Omit<ResourcePermissionType, 'groupId'> & {
groupId: MemberGroupSchemaType;
};
export type PermissionSchemaType = { export type PermissionSchemaType = {
defaultPermission: PermissionValueType; defaultPermission: PermissionValueType;
inheritPermission: boolean; inheritPermission: boolean;

View File

@@ -14,7 +14,8 @@ const staticPluginList = [
`Doc2X/FilePDF2text`, `Doc2X/FilePDF2text`,
`Doc2X/FileImg2text`, `Doc2X/FileImg2text`,
'feishu', 'feishu',
'google' 'google',
'bing'
]; ];
// Run in worker thread (Have npm packages) // Run in worker thread (Have npm packages)
const packagePluginList = [ const packagePluginList = [

View File

@@ -0,0 +1,510 @@
{
"author": "",
"version": "4811",
"name": "Bing搜索",
"avatar": "core/workflow/template/bing",
"intro": "在Bing中搜索。",
"showStatus": true,
"weight": 10,
"courseUrl": "https://fael3z0zfze.feishu.cn/wiki/LsKAwOmtniA4vkkC259cmfxXnAc?fromScene=spaceOverview",
"isTool": true,
"templateType": "search",
"workflow": {
"nodes": [
{
"nodeId": "pluginInput",
"name": "workflow:template.plugin_start",
"intro": "workflow:intro_plugin_input",
"avatar": "core/workflow/template/workflowStart",
"flowNodeType": "pluginInput",
"showStatus": false,
"position": {
"x": 636.3048409085379,
"y": -238.61714728578016
},
"version": "481",
"inputs": [
{
"renderTypeList": ["input"],
"selectedTypeIndex": 0,
"valueType": "string",
"canEdit": true,
"key": "key",
"label": "key",
"description": "bing搜索key",
"defaultValue": "",
"required": true
},
{
"renderTypeList": ["input", "reference"],
"selectedTypeIndex": 0,
"valueType": "string",
"canEdit": true,
"key": "query",
"label": "query",
"description": "查询字段值",
"defaultValue": "",
"list": [
{
"label": "",
"value": ""
}
],
"required": true,
"toolDescription": "查询字段值"
}
],
"outputs": [
{
"id": "key",
"valueType": "string",
"key": "key",
"label": "key",
"type": "hidden"
},
{
"id": "query",
"valueType": "string",
"key": "query",
"label": "query",
"type": "hidden"
}
]
},
{
"nodeId": "pluginOutput",
"name": "common:core.module.template.self_output",
"intro": "workflow:intro_custom_plugin_output",
"avatar": "core/workflow/template/pluginOutput",
"flowNodeType": "pluginOutput",
"showStatus": false,
"position": {
"x": 2764.1105686698083,
"y": -30.617147285780163
},
"version": "481",
"inputs": [
{
"renderTypeList": ["reference"],
"valueType": "object",
"canEdit": true,
"key": "result",
"label": "result",
"isToolOutput": true,
"description": "",
"value": ["pZTkvleFSZXo", "system_rawResponse"]
}
],
"outputs": []
},
{
"nodeId": "pluginConfig",
"name": "common:core.module.template.system_config",
"intro": "",
"avatar": "core/workflow/template/systemConfig",
"flowNodeType": "pluginConfig",
"position": {
"x": 184.66337662472682,
"y": -216.05298493910115
},
"version": "4811",
"inputs": [],
"outputs": []
},
{
"nodeId": "nyA6oA8mF1iW",
"name": "HTTP 请求",
"intro": "调用谷歌搜索,查询相关内容",
"avatar": "core/workflow/template/httpRequest",
"flowNodeType": "httpRequest468",
"showStatus": true,
"position": {
"x": 1335.0647252518884,
"y": -455.9043948565971
},
"version": "481",
"inputs": [
{
"key": "system_addInputParam",
"renderTypeList": ["addInputParam"],
"valueType": "dynamic",
"label": "",
"required": false,
"description": "common:core.module.input.description.HTTP Dynamic Input",
"customInputConfig": {
"selectValueTypeList": [
"string",
"number",
"boolean",
"object",
"arrayString",
"arrayNumber",
"arrayBoolean",
"arrayObject",
"arrayAny",
"any",
"chatHistory",
"datasetQuote",
"dynamic",
"selectApp",
"selectDataset"
],
"showDescription": false,
"showDefaultValue": true
},
"debugLabel": "",
"toolDescription": ""
},
{
"key": "system_httpMethod",
"renderTypeList": ["custom"],
"valueType": "string",
"label": "",
"value": "GET",
"required": true,
"debugLabel": "",
"toolDescription": ""
},
{
"key": "system_httpTimeout",
"renderTypeList": ["custom"],
"valueType": "number",
"label": "",
"value": 30,
"min": 5,
"max": 600,
"required": true,
"debugLabel": "",
"toolDescription": ""
},
{
"key": "system_httpReqUrl",
"renderTypeList": ["hidden"],
"valueType": "string",
"label": "",
"description": "common:core.module.input.description.Http Request Url",
"placeholder": "https://api.ai.com/getInventory",
"required": false,
"value": "https://api.bing.microsoft.com/v7.0/search",
"debugLabel": "",
"toolDescription": ""
},
{
"key": "system_httpHeader",
"renderTypeList": ["custom"],
"valueType": "any",
"value": [
{
"key": "Ocp-Apim-Subscription-Key",
"type": "string",
"value": "{{$pluginInput.key$}}"
}
],
"label": "",
"description": "common:core.module.input.description.Http Request Header",
"placeholder": "common:core.module.input.description.Http Request Header",
"required": false,
"debugLabel": "",
"toolDescription": ""
},
{
"key": "system_httpParams",
"renderTypeList": ["hidden"],
"valueType": "any",
"value": [
{
"key": "q",
"type": "string",
"value": "{{query}}"
}
],
"label": "",
"required": false,
"debugLabel": "",
"toolDescription": ""
},
{
"key": "system_httpJsonBody",
"renderTypeList": ["hidden"],
"valueType": "any",
"value": "",
"label": "",
"required": false,
"debugLabel": "",
"toolDescription": ""
},
{
"key": "system_httpFormBody",
"renderTypeList": ["hidden"],
"valueType": "any",
"value": [],
"label": "",
"required": false,
"debugLabel": "",
"toolDescription": ""
},
{
"key": "system_httpContentType",
"renderTypeList": ["hidden"],
"valueType": "string",
"value": "json",
"label": "",
"required": false,
"debugLabel": "",
"toolDescription": ""
},
{
"valueType": "string",
"renderTypeList": ["reference"],
"key": "query",
"label": "query",
"toolDescription": "谷歌搜索检索词",
"required": true,
"canEdit": true,
"editField": {
"key": true,
"description": true
},
"customInputConfig": {
"selectValueTypeList": [
"string",
"number",
"boolean",
"object",
"arrayString",
"arrayNumber",
"arrayBoolean",
"arrayObject",
"arrayAny",
"any",
"chatHistory",
"datasetQuote",
"dynamic",
"selectApp",
"selectDataset"
],
"showDescription": false,
"showDefaultValue": true
},
"value": ["pluginInput", "query"]
}
],
"outputs": [
{
"id": "error",
"key": "error",
"label": "workflow:request_error",
"description": "HTTP请求错误信息成功时返回空",
"valueType": "object",
"type": "static"
},
{
"id": "httpRawResponse",
"key": "httpRawResponse",
"required": true,
"label": "workflow:raw_response",
"description": "HTTP请求的原始响应。只能接受字符串或JSON类型响应数据。",
"valueType": "any",
"type": "static"
},
{
"id": "system_addOutputParam",
"key": "system_addOutputParam",
"type": "dynamic",
"valueType": "dynamic",
"label": "",
"editField": {
"key": true,
"valueType": true
}
},
{
"id": "M5YmxaYe8em1",
"type": "dynamic",
"key": "prompt",
"valueType": "string",
"label": "prompt"
}
]
},
{
"nodeId": "pZTkvleFSZXo",
"name": "代码运行",
"intro": "执行一段简单的脚本代码,通常用于进行复杂的数据处理。",
"avatar": "core/workflow/template/codeRun",
"flowNodeType": "code",
"showStatus": true,
"position": {
"x": 2153.5325687235554,
"y": -188.04429852303304
},
"version": "482",
"inputs": [
{
"key": "system_addInputParam",
"renderTypeList": ["addInputParam"],
"valueType": "dynamic",
"label": "",
"required": false,
"description": "workflow:these_variables_will_be_input_parameters_for_code_execution",
"editField": {
"key": true,
"valueType": true
},
"customInputConfig": {
"selectValueTypeList": [
"string",
"number",
"boolean",
"object",
"arrayString",
"arrayNumber",
"arrayBoolean",
"arrayObject",
"arrayAny",
"any",
"chatHistory",
"datasetQuote",
"dynamic",
"selectApp",
"selectDataset"
],
"showDescription": false,
"showDefaultValue": true
},
"debugLabel": "",
"toolDescription": ""
},
{
"key": "codeType",
"renderTypeList": ["hidden"],
"label": "",
"value": "js",
"debugLabel": "",
"toolDescription": ""
},
{
"key": "code",
"renderTypeList": ["custom"],
"label": "",
"value": "function main({data}){\n const result = data.webPages.value.map((item) => ({\n title: item.name,\n link: item.url,\n snippet: item.snippet\n }))\n return JSON.stringify(result) \n}",
"debugLabel": "",
"toolDescription": ""
},
{
"key": "data",
"valueType": "object",
"label": "data",
"renderTypeList": ["reference"],
"description": "",
"canEdit": true,
"editField": {
"key": true,
"valueType": true
},
"value": ["nyA6oA8mF1iW", "httpRawResponse"],
"customInputConfig": {
"selectValueTypeList": [
"string",
"number",
"boolean",
"object",
"arrayString",
"arrayNumber",
"arrayBoolean",
"arrayObject",
"arrayAny",
"any",
"chatHistory",
"datasetQuote",
"dynamic",
"selectApp",
"selectDataset"
],
"showDescription": false,
"showDefaultValue": true
}
}
],
"outputs": [
{
"id": "system_rawResponse",
"key": "system_rawResponse",
"label": "workflow:full_response_data",
"valueType": "object",
"type": "static",
"description": ""
},
{
"id": "error",
"key": "error",
"label": "workflow:execution_error",
"description": "代码运行错误信息,成功时返回空",
"valueType": "object",
"type": "static"
},
{
"id": "system_addOutputParam",
"key": "system_addOutputParam",
"type": "dynamic",
"valueType": "dynamic",
"label": "",
"editField": {
"key": true,
"valueType": true
},
"description": "将代码中 return 的对象作为输出,传递给后续的节点"
},
{
"id": "qLUQfhG0ILRX",
"type": "dynamic",
"key": "prompt",
"valueType": "string",
"label": "prompt"
}
]
}
],
"edges": [
{
"source": "pluginInput",
"target": "nyA6oA8mF1iW",
"sourceHandle": "pluginInput-source-right",
"targetHandle": "nyA6oA8mF1iW-target-left"
},
{
"source": "nyA6oA8mF1iW",
"target": "pZTkvleFSZXo",
"sourceHandle": "nyA6oA8mF1iW-source-right",
"targetHandle": "pZTkvleFSZXo-target-left"
},
{
"source": "pZTkvleFSZXo",
"target": "pluginOutput",
"sourceHandle": "pZTkvleFSZXo-source-right",
"targetHandle": "pluginOutput-target-left"
}
],
"chatConfig": {
"welcomeText": "",
"variables": [],
"questionGuide": false,
"ttsConfig": {
"type": "web"
},
"whisperConfig": {
"open": false,
"autoSend": false,
"autoTTSResponse": false
},
"chatInputGuide": {
"open": false,
"textList": [],
"customUrl": ""
},
"instruction": "",
"_id": "6709e90cd9873479ee78fe71"
}
}
}

View File

@@ -6,7 +6,7 @@
"intro": "在google中搜索。", "intro": "在google中搜索。",
"showStatus": true, "showStatus": true,
"weight": 10, "weight": 10,
"courseUrl": "https://fael3z0zfze.feishu.cn/wiki/Vqk1w4ltNiuLifkHTuoc0hSrnVg?fromScene=spaceOverview",
"isTool": true, "isTool": true,
"templateType": "search", "templateType": "search",

View File

@@ -19,8 +19,11 @@ export const NextEntry = ({ beforeCallback = [] }: { beforeCallback?: Promise<an
await Promise.all([withNextCors(req, res), ...beforeCallback]); await Promise.all([withNextCors(req, res), ...beforeCallback]);
let response = null; let response = null;
for (const handler of args) { for await (const handler of args) {
response = await handler(req, res); response = await handler(req, res);
if (res.writableFinished) {
break;
}
} }
// Get request duration // Get request duration

View File

@@ -0,0 +1,32 @@
import { ApiRequestProps } from '../../type/next';
import requestIp from 'request-ip';
import { ERROR_ENUM } from '@fastgpt/global/common/error/errorCode';
import { authFrequencyLimit } from '../system/frequencyLimit/utils';
import { addSeconds } from 'date-fns';
import { NextApiResponse } from 'next';
import { jsonRes } from '../response';
// unit: times/s
// how to use?
// export default NextAPI(useQPSLimit(10), handler); // limit 10 times per second for a ip
export function useReqFrequencyLimit(seconds: number, limit: number) {
return async (req: ApiRequestProps, res: NextApiResponse) => {
const ip = requestIp.getClientIp(req);
if (!ip && process.env.USE_IP_LIMIT !== 'true') {
return;
}
try {
await authFrequencyLimit({
eventId: 'ip-qps-limit' + ip,
maxAmount: limit,
expiredTime: addSeconds(new Date(), seconds)
});
} catch (_) {
res.status(429);
jsonRes(res, {
code: 429,
message: ERROR_ENUM.tooManyRequest
});
}
};
}

View File

@@ -0,0 +1,27 @@
import { getMongoModel, Schema } from '../../mongo';
import type { FrequencyLimitSchemaType } from './type';
const FrequencyLimitSchema = new Schema({
eventId: {
type: String,
required: true
},
amount: {
type: Number,
default: 0
},
expiredTime: {
type: Date,
required: true
}
});
try {
FrequencyLimitSchema.index({ eventId: 1, expiredTime: 1 });
FrequencyLimitSchema.index({ expiredTime: 1 }, { expireAfterSeconds: 0 });
} catch (error) {}
export const MongoFrequencyLimit = getMongoModel<FrequencyLimitSchemaType>(
'frequency_limit',
FrequencyLimitSchema
);

View File

@@ -0,0 +1,6 @@
export type FrequencyLimitSchemaType = {
_id: string;
eventId: string; // 事件ID
amount: number; // 当前数量
expiredTime: Date; // 什么时候过期,过期则重置
};

View File

@@ -0,0 +1,33 @@
import { AuthFrequencyLimitProps } from '@fastgpt/global/common/frequenctLimit/type';
import { MongoFrequencyLimit } from './schema';
export const authFrequencyLimit = async ({
eventId,
maxAmount,
expiredTime
}: AuthFrequencyLimitProps) => {
try {
// 对应 eventId 的 account+1, 不存在的话,则创建一个
const result = await MongoFrequencyLimit.findOneAndUpdate(
{
eventId,
expiredTime: { $gte: new Date() }
},
{
$inc: { amount: 1 },
// If not exist, set the expiredTime
$setOnInsert: { expiredTime }
},
{
upsert: true,
new: true
}
).lean();
// 因为始终会返回+1的结果所以这里不能直接等需要多一个。
if (result.amount > maxAmount) {
return Promise.reject(result);
}
} catch (error) {
console.log(error);
}
};

View File

@@ -5,8 +5,6 @@ import {
TeamCollectionName, TeamCollectionName,
TeamMemberCollectionName TeamMemberCollectionName
} from '@fastgpt/global/support/user/team/constant'; } from '@fastgpt/global/support/user/team/constant';
import { AppDefaultPermissionVal } from '@fastgpt/global/support/permission/app/constant';
import { getPermissionSchema } from '@fastgpt/global/support/permission/utils';
export const AppCollectionName = 'apps'; export const AppCollectionName = 'apps';
@@ -111,8 +109,13 @@ const AppSchema = new Schema({
inited: { inited: {
type: Boolean type: Boolean
}, },
inheritPermission: {
type: Boolean,
default: true
},
...getPermissionSchema(AppDefaultPermissionVal) // abandoned
defaultPermission: Number
}); });
AppSchema.index({ teamId: 1, updateTime: -1 }); AppSchema.index({ teamId: 1, updateTime: -1 });

View File

@@ -9,8 +9,6 @@ import {
TeamCollectionName, TeamCollectionName,
TeamMemberCollectionName TeamMemberCollectionName
} from '@fastgpt/global/support/user/team/constant'; } from '@fastgpt/global/support/user/team/constant';
import { DatasetDefaultPermissionVal } from '@fastgpt/global/support/permission/dataset/constant';
import { getPermissionSchema } from '@fastgpt/global/support/permission/utils';
import type { DatasetSchemaType } from '@fastgpt/global/core/dataset/type.d'; import type { DatasetSchemaType } from '@fastgpt/global/core/dataset/type.d';
export const DatasetCollectionName = 'datasets'; export const DatasetCollectionName = 'datasets';
@@ -88,7 +86,13 @@ const DatasetSchema = new Schema({
externalReadUrl: { externalReadUrl: {
type: String type: String
}, },
...getPermissionSchema(DatasetDefaultPermissionVal) inheritPermission: {
type: Boolean,
default: true
},
// abandoned
defaultPermission: Number
}); });
try { try {

View File

@@ -211,18 +211,7 @@ export const dispatchRunTools = async (props: DispatchToolModuleProps): Promise<
}); });
// flat child tool response // flat child tool response
let newVariables: Record<string, any> = props.variables; const childToolResponse = dispatchFlowResponse.map((item) => item.flowResponses).flat();
const childToolResponse = dispatchFlowResponse
.map((item) => {
// Computed new variables
newVariables = {
...newVariables,
...item.newVariables
};
return item.flowResponses;
})
.flat();
// concat tool usage // concat tool usage
const totalPointsUsage = const totalPointsUsage =
@@ -261,7 +250,6 @@ export const dispatchRunTools = async (props: DispatchToolModuleProps): Promise<
}, },
...flatUsages ...flatUsages
], ],
[DispatchNodeResponseKeyEnum.newVariables]: newVariables,
[DispatchNodeResponseKeyEnum.interactive]: toolWorkflowInteractiveResponse [DispatchNodeResponseKeyEnum.interactive]: toolWorkflowInteractiveResponse
}; };
}; };

View File

@@ -27,7 +27,7 @@ import { getNanoid, sliceStrStartEnd } from '@fastgpt/global/common/string/tools
import { addLog } from '../../../../../common/system/log'; import { addLog } from '../../../../../common/system/log';
import { toolValueTypeList } from '@fastgpt/global/core/workflow/constants'; import { toolValueTypeList } from '@fastgpt/global/core/workflow/constants';
import { WorkflowInteractiveResponseType } from '@fastgpt/global/core/workflow/template/system/interactive/type'; import { WorkflowInteractiveResponseType } from '@fastgpt/global/core/workflow/template/system/interactive/type';
import { ChatItemValueTypeEnum, ChatRoleEnum } from '@fastgpt/global/core/chat/constants'; import { ChatItemValueTypeEnum } from '@fastgpt/global/core/chat/constants';
type ToolRunResponseType = { type ToolRunResponseType = {
toolRunResponse: DispatchFlowResponse; toolRunResponse: DispatchFlowResponse;

View File

@@ -41,7 +41,8 @@ import { dispatchPluginOutput } from './plugin/runOutput';
import { removeSystemVariable, valueTypeFormat } from './utils'; import { removeSystemVariable, valueTypeFormat } from './utils';
import { import {
filterWorkflowEdges, filterWorkflowEdges,
checkNodeRunStatus checkNodeRunStatus,
textAdaptGptResponse
} from '@fastgpt/global/core/workflow/runtime/utils'; } from '@fastgpt/global/core/workflow/runtime/utils';
import { ChatNodeUsageType } from '@fastgpt/global/support/wallet/bill/type'; import { ChatNodeUsageType } from '@fastgpt/global/support/wallet/bill/type';
import { dispatchRunTools } from './agent/runTool/index'; import { dispatchRunTools } from './agent/runTool/index';
@@ -161,6 +162,20 @@ export async function dispatchWorkFlow(data: Props): Promise<DispatchFlowRespons
res.setHeader('Access-Control-Allow-Origin', '*'); res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('X-Accel-Buffering', 'no'); res.setHeader('X-Accel-Buffering', 'no');
res.setHeader('Cache-Control', 'no-cache, no-transform'); res.setHeader('Cache-Control', 'no-cache, no-transform');
// 10s sends a message to prevent the browser from thinking that the connection is disconnected
const sendStreamTimerSign = () => {
setTimeout(() => {
props?.workflowStreamResponse?.({
event: SseResponseEventEnum.answer,
data: textAdaptGptResponse({
text: ''
})
});
sendStreamTimerSign();
}, 10000);
};
sendStreamTimerSign();
} }
variables = { variables = {
@@ -592,56 +607,60 @@ export async function dispatchWorkFlow(data: Props): Promise<DispatchFlowRespons
}; };
} }
// start process width initInput try {
const entryNodes = runtimeNodes.filter((item) => item.isEntry); // start process width initInput
// reset entry const entryNodes = runtimeNodes.filter((item) => item.isEntry);
runtimeNodes.forEach((item) => { // reset entry
// Interactive node is not the entry node, return interactive result runtimeNodes.forEach((item) => {
if ( // Interactive node is not the entry node, return interactive result
item.flowNodeType !== FlowNodeTypeEnum.userSelect && if (
item.flowNodeType !== FlowNodeTypeEnum.formInput && item.flowNodeType !== FlowNodeTypeEnum.userSelect &&
item.flowNodeType !== FlowNodeTypeEnum.tools item.flowNodeType !== FlowNodeTypeEnum.formInput &&
) { item.flowNodeType !== FlowNodeTypeEnum.tools
item.isEntry = false; ) {
} item.isEntry = false;
}); }
await Promise.all(entryNodes.map((node) => checkNodeCanRun(node))); });
await Promise.all(entryNodes.map((node) => checkNodeCanRun(node)));
// focus try to run pluginOutput // focus try to run pluginOutput
const pluginOutputModule = runtimeNodes.find( const pluginOutputModule = runtimeNodes.find(
(item) => item.flowNodeType === FlowNodeTypeEnum.pluginOutput (item) => item.flowNodeType === FlowNodeTypeEnum.pluginOutput
); );
if (pluginOutputModule && props.mode !== 'debug') { if (pluginOutputModule && props.mode !== 'debug') {
await nodeRunWithActive(pluginOutputModule); await nodeRunWithActive(pluginOutputModule);
}
// Interactive node
const interactiveResult = (() => {
if (nodeInteractiveResponse) {
const interactiveAssistant = handleInteractiveResult({
entryNodeIds: nodeInteractiveResponse.entryNodeIds,
interactiveResponse: nodeInteractiveResponse.interactiveResponse
});
chatAssistantResponse.push(interactiveAssistant);
return interactiveAssistant.interactive;
}
})();
return {
flowResponses: chatResponses,
flowUsages: chatNodeUsages,
debugResponse: {
finishedNodes: runtimeNodes,
finishedEdges: runtimeEdges,
nextStepRunNodes: debugNextStepRunNodes
},
workflowInteractiveResponse: interactiveResult,
[DispatchNodeResponseKeyEnum.runTimes]: workflowRunTimes,
[DispatchNodeResponseKeyEnum.assistantResponses]:
mergeAssistantResponseAnswerText(chatAssistantResponse),
[DispatchNodeResponseKeyEnum.toolResponses]: toolRunResponse,
newVariables: removeSystemVariable(variables)
};
} catch (error) {
return Promise.reject(error);
} }
// Interactive node
const interactiveResult = (() => {
if (nodeInteractiveResponse) {
const interactiveAssistant = handleInteractiveResult({
entryNodeIds: nodeInteractiveResponse.entryNodeIds,
interactiveResponse: nodeInteractiveResponse.interactiveResponse
});
chatAssistantResponse.push(interactiveAssistant);
return interactiveAssistant.interactive;
}
})();
return {
flowResponses: chatResponses,
flowUsages: chatNodeUsages,
debugResponse: {
finishedNodes: runtimeNodes,
finishedEdges: runtimeEdges,
nextStepRunNodes: debugNextStepRunNodes
},
workflowInteractiveResponse: interactiveResult,
[DispatchNodeResponseKeyEnum.runTimes]: workflowRunTimes,
[DispatchNodeResponseKeyEnum.assistantResponses]:
mergeAssistantResponseAnswerText(chatAssistantResponse),
[DispatchNodeResponseKeyEnum.toolResponses]: toolRunResponse,
newVariables: removeSystemVariable(variables)
};
} }
/* get system variable */ /* get system variable */

View File

@@ -7,6 +7,7 @@ import {
import { dispatchWorkFlow } from '..'; import { dispatchWorkFlow } from '..';
import { DispatchNodeResponseKeyEnum } from '@fastgpt/global/core/workflow/runtime/constants'; import { DispatchNodeResponseKeyEnum } from '@fastgpt/global/core/workflow/runtime/constants';
import { AIChatItemValueItemType, ChatHistoryItemResType } from '@fastgpt/global/core/chat/type'; import { AIChatItemValueItemType, ChatHistoryItemResType } from '@fastgpt/global/core/chat/type';
import { cloneDeep } from 'lodash';
type Props = ModuleDispatchProps<{ type Props = ModuleDispatchProps<{
[NodeInputKeyEnum.loopInputArray]: Array<any>; [NodeInputKeyEnum.loopInputArray]: Array<any>;
@@ -19,6 +20,7 @@ type Response = DispatchNodeResultType<{
export const dispatchLoop = async (props: Props): Promise<Response> => { export const dispatchLoop = async (props: Props): Promise<Response> => {
const { const {
params, params,
runtimeEdges,
runtimeNodes, runtimeNodes,
user, user,
node: { name } node: { name }
@@ -28,12 +30,13 @@ export const dispatchLoop = async (props: Props): Promise<Response> => {
if (!Array.isArray(loopInputArray)) { if (!Array.isArray(loopInputArray)) {
return Promise.reject('Input value is not an array'); return Promise.reject('Input value is not an array');
} }
if (loopInputArray.length > 50) { const maxLength = process.env.WORKFLOW_MAX_LOOP_TIMES
? Number(process.env.WORKFLOW_MAX_LOOP_TIMES)
: 50;
if (loopInputArray.length > maxLength) {
return Promise.reject('Input array length cannot be greater than 50'); return Promise.reject('Input array length cannot be greater than 50');
} }
const runNodes = runtimeNodes.filter((node) => childrenNodeIdList.includes(node.nodeId));
const outputValueArr = []; const outputValueArr = [];
const loopDetail: ChatHistoryItemResType[] = []; const loopDetail: ChatHistoryItemResType[] = [];
let assistantResponses: AIChatItemValueItemType[] = []; let assistantResponses: AIChatItemValueItemType[] = [];
@@ -41,27 +44,25 @@ export const dispatchLoop = async (props: Props): Promise<Response> => {
let newVariables: Record<string, any> = props.variables; let newVariables: Record<string, any> = props.variables;
for await (const item of loopInputArray) { for await (const item of loopInputArray) {
runtimeNodes.forEach((node) => {
if (
childrenNodeIdList.includes(node.nodeId) &&
node.flowNodeType === FlowNodeTypeEnum.loopStart
) {
node.isEntry = true;
node.inputs = node.inputs.map((input) =>
input.key === NodeInputKeyEnum.loopStartInput
? {
...input,
value: item
}
: input
);
}
});
const response = await dispatchWorkFlow({ const response = await dispatchWorkFlow({
...props, ...props,
runtimeNodes: runNodes.map((node) => runtimeEdges: cloneDeep(runtimeEdges)
node.flowNodeType === FlowNodeTypeEnum.loopStart
? {
...node,
isEntry: true,
inputs: node.inputs.map((input) =>
input.key === NodeInputKeyEnum.loopStartInput
? {
...input,
value: item
}
: input
)
}
: {
...node,
isEntry: false
}
)
}); });
const loopOutputValue = response.flowResponses.find( const loopOutputValue = response.flowResponses.find(

View File

@@ -113,11 +113,10 @@ export const dispatchRunPlugin = async (props: RunPluginProps): Promise<RunPlugi
} }
const usagePoints = await computedPluginUsage(plugin, flowUsages); const usagePoints = await computedPluginUsage(plugin, flowUsages);
const childStreamResponse = system_forbid_stream ? false : props.stream;
return { return {
// 嵌套运行时,如果 childApp stream=false实际上不会有任何内容输出给用户所以不需要存储 // 嵌套运行时,如果 childApp stream=false实际上不会有任何内容输出给用户所以不需要存储
assistantResponses: childStreamResponse ? assistantResponses : [], assistantResponses: system_forbid_stream ? [] : assistantResponses,
// responseData, // debug // responseData, // debug
[DispatchNodeResponseKeyEnum.runTimes]: runTimes, [DispatchNodeResponseKeyEnum.runTimes]: runTimes,
[DispatchNodeResponseKeyEnum.nodeResponse]: { [DispatchNodeResponseKeyEnum.nodeResponse]: {

View File

@@ -124,7 +124,7 @@ export const dispatchRunAppNode = async (props: Props): Promise<Response> => {
const usagePoints = flowUsages.reduce((sum, item) => sum + (item.totalPoints || 0), 0); const usagePoints = flowUsages.reduce((sum, item) => sum + (item.totalPoints || 0), 0);
return { return {
assistantResponses: childStreamResponse ? assistantResponses : [], assistantResponses: system_forbid_stream ? [] : assistantResponses,
[DispatchNodeResponseKeyEnum.runTimes]: runTimes, [DispatchNodeResponseKeyEnum.runTimes]: runTimes,
[DispatchNodeResponseKeyEnum.nodeResponse]: { [DispatchNodeResponseKeyEnum.nodeResponse]: {
moduleLogo: appData.avatar, moduleLogo: appData.avatar,

View File

@@ -232,10 +232,23 @@ export const dispatchHttp468Request = async (props: HttpRequestProps): Promise<H
// format output value type // format output value type
const results: Record<string, any> = {}; const results: Record<string, any> = {};
node.outputs.forEach((item) => { node.outputs
const key = item.key.startsWith('$') ? item.key : `$.${item.key}`; .filter(
results[item.key] = JSONPath({ path: key, json: formatResponse })[0]; (item) =>
}); item.key !== NodeOutputKeyEnum.error && item.key !== NodeOutputKeyEnum.httpRawResponse
)
.forEach((item) => {
const key = item.key.startsWith('$') ? item.key : `$.${item.key}`;
results[item.key] = (() => {
// 检查是否是简单的属性访问或单一索引访问
if (/^\$(\.[a-zA-Z_][a-zA-Z0-9_]*)+$/.test(key) || /^\$(\[\d+\])+$/.test(key)) {
return JSONPath({ path: key, json: formatResponse })[0];
}
// 如果无法确定,默认返回数组
return JSONPath({ path: key, json: formatResponse });
})();
});
if (typeof formatResponse[NodeOutputKeyEnum.answerText] === 'string') { if (typeof formatResponse[NodeOutputKeyEnum.answerText] === 'string') {
workflowStreamResponse?.({ workflowStreamResponse?.({
@@ -247,6 +260,7 @@ export const dispatchHttp468Request = async (props: HttpRequestProps): Promise<H
} }
return { return {
...results,
[DispatchNodeResponseKeyEnum.nodeResponse]: { [DispatchNodeResponseKeyEnum.nodeResponse]: {
totalPoints: 0, totalPoints: 0,
params: Object.keys(params).length > 0 ? params : undefined, params: Object.keys(params).length > 0 ? params : undefined,
@@ -256,8 +270,7 @@ export const dispatchHttp468Request = async (props: HttpRequestProps): Promise<H
}, },
[DispatchNodeResponseKeyEnum.toolResponses]: [DispatchNodeResponseKeyEnum.toolResponses]:
Object.keys(results).length > 0 ? results : rawResponse, Object.keys(results).length > 0 ? results : rawResponse,
[NodeOutputKeyEnum.httpRawResponse]: rawResponse, [NodeOutputKeyEnum.httpRawResponse]: rawResponse
...results
}; };
} catch (error) { } catch (error) {
addLog.error('Http request error', error); addLog.error('Http request error', error);

View File

@@ -33,6 +33,7 @@
"papaparse": "5.4.1", "papaparse": "5.4.1",
"pdfjs-dist": "4.4.168", "pdfjs-dist": "4.4.168",
"pg": "^8.10.0", "pg": "^8.10.0",
"request-ip": "^3.3.0",
"tiktoken": "^1.0.15", "tiktoken": "^1.0.15",
"tunnel": "^0.0.6", "tunnel": "^0.0.6",
"turndown": "^7.1.2" "turndown": "^7.1.2"
@@ -46,6 +47,7 @@
"@types/node-cron": "^3.0.11", "@types/node-cron": "^3.0.11",
"@types/papaparse": "5.3.7", "@types/papaparse": "5.3.7",
"@types/pg": "^8.6.6", "@types/pg": "^8.6.6",
"@types/request-ip": "^0.0.37",
"@types/tunnel": "^0.0.4", "@types/tunnel": "^0.0.4",
"@types/turndown": "^5.0.4" "@types/turndown": "^5.0.4"
} }

View File

@@ -13,6 +13,7 @@ import { ParentIdType } from '@fastgpt/global/common/parentFolder/type';
import { splitCombinePluginId } from '../../../core/app/plugin/controller'; import { splitCombinePluginId } from '../../../core/app/plugin/controller';
import { PluginSourceEnum } from '@fastgpt/global/core/plugin/constants'; import { PluginSourceEnum } from '@fastgpt/global/core/plugin/constants';
import { AuthModeType, AuthResponseType } from '../type'; import { AuthModeType, AuthResponseType } from '../type';
import { AppDefaultPermissionVal } from '@fastgpt/global/support/permission/app/constant';
export const authPluginByTmbId = async ({ export const authPluginByTmbId = async ({
tmbId, tmbId,
@@ -60,7 +61,6 @@ export const authAppByTmbId = async ({
if (isRoot) { if (isRoot) {
return { return {
...app, ...app,
defaultPermission: app.defaultPermission,
permission: new AppPermission({ isOwner: true }) permission: new AppPermission({ isOwner: true })
}; };
} }
@@ -71,7 +71,7 @@ export const authAppByTmbId = async ({
const isOwner = tmbPer.isOwner || String(app.tmbId) === String(tmbId); const isOwner = tmbPer.isOwner || String(app.tmbId) === String(tmbId);
const { Per, defaultPermission } = await (async () => { const { Per } = await (async () => {
if ( if (
AppFolderTypeList.includes(app.type) || AppFolderTypeList.includes(app.type) ||
app.inheritPermission === false || app.inheritPermission === false ||
@@ -86,10 +86,9 @@ export const authAppByTmbId = async ({
resourceId: appId, resourceId: appId,
resourceType: PerResourceTypeEnum.app resourceType: PerResourceTypeEnum.app
}); });
const Per = new AppPermission({ per: rp ?? app.defaultPermission, isOwner }); const Per = new AppPermission({ per: rp ?? AppDefaultPermissionVal, isOwner });
return { return {
Per, Per
defaultPermission: app.defaultPermission
}; };
} else { } else {
// is not folder and inheritPermission is true and is not root folder. // is not folder and inheritPermission is true and is not root folder.
@@ -104,8 +103,7 @@ export const authAppByTmbId = async ({
isOwner isOwner
}); });
return { return {
Per, Per
defaultPermission: parent.defaultPermission
}; };
} }
})(); })();
@@ -116,7 +114,6 @@ export const authAppByTmbId = async ({
return { return {
...app, ...app,
defaultPermission,
permission: Per permission: Per
}; };
})(); })();

View File

@@ -10,12 +10,17 @@ import { MongoResourcePermission } from './schema';
import { ClientSession } from 'mongoose'; import { ClientSession } from 'mongoose';
import { import {
PermissionValueType, PermissionValueType,
ResourcePermissionType ResourcePermissionType,
ResourcePerWithGroup,
ResourcePerWithTmbWithUser
} from '@fastgpt/global/support/permission/type'; } from '@fastgpt/global/support/permission/type';
import { bucketNameMap } from '@fastgpt/global/common/file/constants'; import { bucketNameMap } from '@fastgpt/global/common/file/constants';
import { addMinutes } from 'date-fns'; import { addMinutes } from 'date-fns';
import { getGroupsByTmbId } from './memberGroup/controllers'; import { getGroupsByTmbId } from './memberGroup/controllers';
import { Permission } from '@fastgpt/global/support/permission/controller'; import { Permission } from '@fastgpt/global/support/permission/controller';
import { ParentIdType } from '@fastgpt/global/common/parentFolder/type';
import { RequireOnlyOne } from '@fastgpt/global/common/type/utils';
import { CommonErrEnum } from '@fastgpt/global/common/error/code/common';
/** get resource permission for a team member /** get resource permission for a team member
* If there is no permission for the team member, it will return undefined * If there is no permission for the team member, it will return undefined
@@ -123,20 +128,94 @@ export async function getResourceAllClbs({
).lean(); ).lean();
} }
export async function getResourceClbsAndGroups({
resourceId,
resourceType,
teamId,
session
}: {
resourceId: ParentIdType;
resourceType: Omit<`${PerResourceTypeEnum}`, 'team'>;
teamId: string;
session: ClientSession;
}) {
return MongoResourcePermission.find(
{
resourceId,
resourceType,
teamId
},
undefined,
{ session }
).lean();
}
export const getClbsAndGroupsWithInfo = async ({
resourceId,
resourceType,
teamId
}: {
resourceId: ParentIdType;
resourceType: Omit<`${PerResourceTypeEnum}`, 'team'>;
teamId: string;
}) =>
Promise.all([
(await MongoResourcePermission.find({
teamId,
resourceId,
resourceType,
tmbId: {
$exists: true
}
}).populate({
path: 'tmbId',
select: 'name userId',
populate: {
path: 'userId',
select: 'avatar'
}
})) as ResourcePerWithTmbWithUser[],
(await MongoResourcePermission.find({
teamId,
resourceId,
resourceType,
groupId: {
$exists: true
}
}).populate({
path: 'groupId',
select: 'name avatar'
})) as ResourcePerWithGroup[]
]);
export const delResourcePermissionById = (id: string) => { export const delResourcePermissionById = (id: string) => {
return MongoResourcePermission.findByIdAndRemove(id); return MongoResourcePermission.findByIdAndRemove(id);
}; };
export const delResourcePermission = ({ export const delResourcePermission = ({
session, session,
tmbId,
groupId,
...props ...props
}: { }: {
resourceType: PerResourceTypeEnum; resourceType: PerResourceTypeEnum;
teamId: string; teamId: string;
resourceId: string; resourceId: string;
tmbId: string;
session?: ClientSession; session?: ClientSession;
tmbId?: string;
groupId?: string;
}) => { }) => {
return MongoResourcePermission.deleteOne(props, { session }); // tmbId or groupId only one and not both
if (!!tmbId === !!groupId) {
return Promise.reject(CommonErrEnum.missingParams);
}
return MongoResourcePermission.deleteOne(
{
...(tmbId ? { tmbId } : {}),
...(groupId ? { groupId } : {}),
...props
},
{ session }
);
}; };
/* 下面代码等迁移 */ /* 下面代码等迁移 */

View File

@@ -20,6 +20,7 @@ import { MongoDatasetData } from '../../../core/dataset/data/schema';
import { AuthModeType, AuthResponseType } from '../type'; import { AuthModeType, AuthResponseType } from '../type';
import { DatasetTypeEnum } from '@fastgpt/global/core/dataset/constants'; import { DatasetTypeEnum } from '@fastgpt/global/core/dataset/constants';
import { ParentIdType } from '@fastgpt/global/common/parentFolder/type'; import { ParentIdType } from '@fastgpt/global/common/parentFolder/type';
import { DatasetDefaultPermissionVal } from '@fastgpt/global/support/permission/dataset/constant';
export const authDatasetByTmbId = async ({ export const authDatasetByTmbId = async ({
tmbId, tmbId,
@@ -62,7 +63,7 @@ export const authDatasetByTmbId = async ({
const isOwner = tmbPer.isOwner || String(dataset.tmbId) === String(tmbId); const isOwner = tmbPer.isOwner || String(dataset.tmbId) === String(tmbId);
// get dataset permission or inherit permission from parent folder. // get dataset permission or inherit permission from parent folder.
const { Per, defaultPermission } = await (async () => { const { Per } = await (async () => {
if ( if (
dataset.type === DatasetTypeEnum.folder || dataset.type === DatasetTypeEnum.folder ||
dataset.inheritPermission === false || dataset.inheritPermission === false ||
@@ -78,12 +79,11 @@ export const authDatasetByTmbId = async ({
resourceType: PerResourceTypeEnum.dataset resourceType: PerResourceTypeEnum.dataset
}); });
const Per = new DatasetPermission({ const Per = new DatasetPermission({
per: rp ?? dataset.defaultPermission, per: rp ?? DatasetDefaultPermissionVal,
isOwner isOwner
}); });
return { return {
Per, Per
defaultPermission: dataset.defaultPermission
}; };
} else { } else {
// is not folder and inheritPermission is true and is not root folder. // is not folder and inheritPermission is true and is not root folder.
@@ -100,8 +100,7 @@ export const authDatasetByTmbId = async ({
}); });
return { return {
Per, Per
defaultPermission: parent.defaultPermission
}; };
} }
})(); })();
@@ -112,7 +111,6 @@ export const authDatasetByTmbId = async ({
return { return {
...dataset, ...dataset,
defaultPermission,
permission: Per permission: Per
}; };
})(); })();
@@ -179,14 +177,15 @@ export async function authDatasetCollection({
tmbId, tmbId,
datasetId: collection.datasetId._id, datasetId: collection.datasetId._id,
per, per,
isRoot: isRootFromHeader || isRoot isRoot: isRootFromHeader
}); });
return { return {
teamId, teamId,
tmbId, tmbId,
collection, collection,
permission: dataset.permission permission: dataset.permission,
isRoot: isRootFromHeader
}; };
} }
@@ -231,7 +230,8 @@ export async function authDatasetFile({
teamId, teamId,
tmbId, tmbId,
file, file,
permission permission,
isRoot
}; };
} catch (error) { } catch (error) {
return Promise.reject(DatasetErrEnum.unAuthDatasetFile); return Promise.reject(DatasetErrEnum.unAuthDatasetFile);

View File

@@ -1,9 +1,9 @@
import { mongoSessionRun } from '../../common/mongo/sessionRun'; import { mongoSessionRun } from '../../common/mongo/sessionRun';
import { MongoResourcePermission } from './schema'; import { MongoResourcePermission } from './schema';
import { ClientSession, Model } from 'mongoose'; import { ClientSession, Model } from 'mongoose';
import { NullPermission, PerResourceTypeEnum } from '@fastgpt/global/support/permission/constant'; import { PerResourceTypeEnum } from '@fastgpt/global/support/permission/constant';
import { PermissionValueType } from '@fastgpt/global/support/permission/type'; import { PermissionValueType } from '@fastgpt/global/support/permission/type';
import { getResourceAllClbs } from './controller'; import { getResourceClbsAndGroups } from './controller';
import { RequireOnlyOne } from '@fastgpt/global/common/type/utils'; import { RequireOnlyOne } from '@fastgpt/global/common/type/utils';
import { ParentIdType } from '@fastgpt/global/common/parentFolder/type'; import { ParentIdType } from '@fastgpt/global/common/parentFolder/type';
@@ -28,7 +28,6 @@ export async function syncChildrenPermission({
resourceModel, resourceModel,
session, session,
defaultPermission,
collaborators collaborators
}: { }: {
resource: SyncChildrenPermissionResourceType; resource: SyncChildrenPermissionResourceType;
@@ -42,7 +41,6 @@ export async function syncChildrenPermission({
// should be provided when inheritPermission is true // should be provided when inheritPermission is true
session: ClientSession; session: ClientSession;
defaultPermission?: PermissionValueType;
collaborators?: UpdateCollaboratorItem[]; collaborators?: UpdateCollaboratorItem[];
}) { }) {
// only folder has permission // only folder has permission
@@ -76,19 +74,6 @@ export async function syncChildrenPermission({
} }
if (!children.length) return; if (!children.length) return;
// Sync default permission
if (defaultPermission !== undefined) {
await resourceModel.updateMany(
{
_id: { $in: children }
},
{
defaultPermission
},
{ session }
);
}
// sync the resource permission // sync the resource permission
if (collaborators) { if (collaborators) {
// Update the collaborators of all children // Update the collaborators of all children
@@ -124,28 +109,20 @@ export async function resumeInheritPermission({
const isFolder = folderTypeList.includes(resource.type); const isFolder = folderTypeList.includes(resource.type);
const fn = async (session: ClientSession) => { const fn = async (session: ClientSession) => {
const parentResource = await resourceModel
.findById(resource.parentId, 'defaultPermission')
.lean<SyncChildrenPermissionResourceType & { defaultPermission: PermissionValueType }>()
.session(session);
const parentDefaultPermissionVal = parentResource?.defaultPermission ?? NullPermission;
// update the resource permission // update the resource permission
await resourceModel.updateOne( await resourceModel.updateOne(
{ {
_id: resource._id _id: resource._id
}, },
{ {
inheritPermission: true, inheritPermission: true
defaultPermission: parentDefaultPermissionVal
}, },
{ session } { session }
); );
// Folder resource, need to sync children // Folder resource, need to sync children
if (isFolder) { if (isFolder) {
const parentClbs = await getResourceAllClbs({ const parentClbsAndGroups = await getResourceClbsAndGroups({
resourceId: resource.parentId, resourceId: resource.parentId,
teamId: resource.teamId, teamId: resource.teamId,
resourceType, resourceType,
@@ -155,7 +132,7 @@ export async function resumeInheritPermission({
// sync self // sync self
await syncCollaborators({ await syncCollaborators({
resourceType, resourceType,
collaborators: parentClbs, collaborators: parentClbsAndGroups,
teamId: resource.teamId, teamId: resource.teamId,
resourceId: resource._id, resourceId: resource._id,
session session
@@ -169,8 +146,7 @@ export async function resumeInheritPermission({
folderTypeList, folderTypeList,
resourceType, resourceType,
session, session,
defaultPermission: parentDefaultPermissionVal, collaborators: parentClbsAndGroups
collaborators: parentClbs
}); });
} else { } else {
// Not folder, delete all clb // Not folder, delete all clb
@@ -215,6 +191,7 @@ export async function syncCollaborators({
resourceId, resourceId,
resourceType: resourceType, resourceType: resourceType,
tmbId: item.tmbId, tmbId: item.tmbId,
groupId: item.groupId,
permission: item.permission permission: item.permission
})), })),
{ {

View File

@@ -64,7 +64,7 @@ export const getGroupsByTmbId = async ({
groupId: { groupId: {
$exists: true $exists: true
}, },
role: role ? { $in: role } : undefined ...(role ? { role: { $in: role } } : {})
}) })
.populate('groupId') .populate('groupId')
.lean() .lean()

View File

@@ -28,5 +28,6 @@ export type AuthResponseType<T extends Permission = Permission> = {
authType?: `${AuthUserTypeEnum}`; authType?: `${AuthUserTypeEnum}`;
appId?: string; appId?: string;
apikey?: string; apikey?: string;
isRoot: boolean;
permission: T; permission: T;
}; };

View File

@@ -8,7 +8,7 @@ import { TeamPermission } from '@fastgpt/global/support/permission/user/controll
/* auth user role */ /* auth user role */
export async function authUserPer(props: AuthModeType): Promise< export async function authUserPer(props: AuthModeType): Promise<
AuthResponseType & { AuthResponseType<TeamPermission> & {
tmb: TeamTmbItemType; tmb: TeamTmbItemType;
} }
> { > {

View File

@@ -232,6 +232,7 @@ export const iconPaths = {
'core/workflow/template/datasource': () => 'core/workflow/template/datasource': () =>
import('./icons/core/workflow/template/datasource.svg'), import('./icons/core/workflow/template/datasource.svg'),
'core/workflow/template/google': () => import('./icons/core/workflow/template/google.svg'), 'core/workflow/template/google': () => import('./icons/core/workflow/template/google.svg'),
'core/workflow/template/bing': () => import('./icons/core/workflow/template/bing.svg'),
'core/workflow/template/fetchUrl': () => import('./icons/core/workflow/template/fetchUrl.svg'), 'core/workflow/template/fetchUrl': () => import('./icons/core/workflow/template/fetchUrl.svg'),
'core/workflow/template/formInput': () => import('./icons/core/workflow/template/formInput.svg'), 'core/workflow/template/formInput': () => import('./icons/core/workflow/template/formInput.svg'),
'core/workflow/template/getTime': () => import('./icons/core/workflow/template/getTime.svg'), 'core/workflow/template/getTime': () => import('./icons/core/workflow/template/getTime.svg'),

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" x="0px" y="0px" width="100" height="100" viewBox="0 0 48 48">
<linearGradient id="ZkmZ8eVihrQTUnr9TZpNla_pOADWgX6vV63_gr1" x1="11.905" x2="17.941" y1="1.952" y2="40.401" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#3dbffc"></stop><stop offset="1" stop-color="#183efb"></stop></linearGradient><path fill="url(#ZkmZ8eVihrQTUnr9TZpNla_pOADWgX6vV63_gr1)" d="M17.572,37.076L20,35.619V10.603c0-1.632-0.796-3.161-2.133-4.096L12.36,2.652 C11.366,1.956,10,2.667,10,3.881V32.5c0,0.22,0.02,0.555,0.033,0.772C10.369,36.867,14.382,38.99,17.572,37.076z"></path><linearGradient id="ZkmZ8eVihrQTUnr9TZpNlb_pOADWgX6vV63_gr2" x1="14.342" x2="34.121" y1="41.478" y2="25.575" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#33bef0"></stop><stop offset=".159" stop-color="#32b9f0"></stop><stop offset=".341" stop-color="#2facf2"></stop><stop offset=".533" stop-color="#2a95f4"></stop><stop offset=".733" stop-color="#2475f6"></stop><stop offset=".936" stop-color="#1b4cfa"></stop><stop offset="1" stop-color="#183efb"></stop></linearGradient><path fill="url(#ZkmZ8eVihrQTUnr9TZpNlb_pOADWgX6vV63_gr2)" d="M32.682,27.904L20,35.5v0l-2.428,1.457c-3.191,1.915-7.203-0.209-7.54-3.804 C10.372,38.922,15.145,43.5,21,43.5c1.963,0,3.888-0.536,5.568-1.551l6.834-4.126c0.817-0.493,1.522-1.075,2.15-1.707 C37.906,33.415,36.739,28.669,32.682,27.904z"></path><linearGradient id="ZkmZ8eVihrQTUnr9TZpNlc_pOADWgX6vV63_gr3" x1="24.223" x2="45.699" y1="17.113" y2="38.588" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#3dbffd"></stop><stop offset="1" stop-color="#1de9b6"></stop></linearGradient><path fill="url(#ZkmZ8eVihrQTUnr9TZpNlc_pOADWgX6vV63_gr3)" d="M33.636,19.568l-7.607-3.803c-1.234-0.617-2.576,0.618-2.064,1.899l1.755,5.886 c0.499,1.248,1.479,2.242,2.719,2.758L32.5,28c4.057,0.766,5.352,5.251,3.052,8.117C40.399,31.24,40.088,22.794,33.636,19.568z"></path>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@@ -71,6 +71,7 @@
"modules.Title is required": "模块名不能为空", "modules.Title is required": "模块名不能为空",
"month.unit": "号", "month.unit": "号",
"move_app": "移动应用", "move_app": "移动应用",
"move.hint": "移动后,所选应用/文件夹将继承新文件夹的权限设置,原先的权限设置失效。",
"not_json_file": "请选择JSON文件", "not_json_file": "请选择JSON文件",
"or_drag_JSON": "或拖入JSON文件", "or_drag_JSON": "或拖入JSON文件",
"paste_config": "粘贴配置", "paste_config": "粘贴配置",

View File

@@ -20,6 +20,7 @@
"Folder": "文件夹", "Folder": "文件夹",
"Login": "登录", "Login": "登录",
"Move": "移动", "Move": "移动",
"move.confirm": "确认移动",
"Name": "名称", "Name": "名称",
"None": "无", "None": "无",
"Rename": "重命名", "Rename": "重命名",
@@ -49,6 +50,7 @@
"code_error.error_code.406": "请求格式错误", "code_error.error_code.406": "请求格式错误",
"code_error.error_code.410": "资源已删除", "code_error.error_code.410": "资源已删除",
"code_error.error_code.422": "验证错误", "code_error.error_code.422": "验证错误",
"code_error.error_code.429": "请求过于频繁",
"code_error.error_code.500": "服务器发生错误", "code_error.error_code.500": "服务器发生错误",
"code_error.error_code.502": "网关错误", "code_error.error_code.502": "网关错误",
"code_error.error_code.503": "服务器暂时过载或正在维护", "code_error.error_code.503": "服务器暂时过载或正在维护",
@@ -81,6 +83,8 @@
"code_error.team_error.un_auth": "无权操作该团队", "code_error.team_error.un_auth": "无权操作该团队",
"code_error.team_error.user_not_active": "用户未接受或已离开团队", "code_error.team_error.user_not_active": "用户未接受或已离开团队",
"code_error.team_error.website_sync_not_enough": "无权使用Web站点同步~", "code_error.team_error.website_sync_not_enough": "无权使用Web站点同步~",
"code_error.team_error.group_name_duplicate": "群组名称重复",
"code_error.team_error.user_not_active": "用户未接受或已离开团队",
"code_error.token_error_code.403": "登录状态无效,请重新登录", "code_error.token_error_code.403": "登录状态无效,请重新登录",
"code_error.user_error.balance_not_enough": "账号余额不足~", "code_error.user_error.balance_not_enough": "账号余额不足~",
"code_error.user_error.bin_visitor": "您的身份校验未通过", "code_error.user_error.bin_visitor": "您的身份校验未通过",
@@ -914,7 +918,7 @@
"permission.Permission config": "权限配置", "permission.Permission config": "权限配置",
"permission.Private": "私有", "permission.Private": "私有",
"permission.Private Tip": "仅自己可用", "permission.Private Tip": "仅自己可用",
"permission.Public": "团队", "permission.Public": "协作",
"permission.Public Tip": "团队所有成员可使用", "permission.Public Tip": "团队所有成员可使用",
"permission.Remove InheritPermission Confirm": "此操作会导致权限继承失效,是否进行?", "permission.Remove InheritPermission Confirm": "此操作会导致权限继承失效,是否进行?",
"permission.Resume InheritPermission Confirm": "是否恢复为继承父级文件夹的权限?", "permission.Resume InheritPermission Confirm": "是否恢复为继承父级文件夹的权限?",
@@ -1193,7 +1197,7 @@
"user.team.invite.Reject Confirm": "确认拒绝该邀请?", "user.team.invite.Reject Confirm": "确认拒绝该邀请?",
"user.team.invite.accept": "接受", "user.team.invite.accept": "接受",
"user.team.invite.reject": "拒绝", "user.team.invite.reject": "拒绝",
"user.team.member.Confirm Leave": "确认离开该团队?", "user.team.member.Confirm Leave": "确认离开该团队?\n退出后您在该团队所有的资源 应用、知识库、文件夹、管理的群组等)均转让给团队所有者。",
"user.team.member.active": "已加入", "user.team.member.active": "已加入",
"user.team.member.reject": "拒绝", "user.team.member.reject": "拒绝",
"user.team.member.waiting": "待接受", "user.team.member.waiting": "待接受",

View File

@@ -34,5 +34,6 @@
"website_dataset_desc": "Web 站点同步允许你直接使用一个网页链接构建知识库", "website_dataset_desc": "Web 站点同步允许你直接使用一个网页链接构建知识库",
"permission.des.read": "可查看知识库内容", "permission.des.read": "可查看知识库内容",
"permission.des.write": "可增加和变更知识库内容", "permission.des.write": "可增加和变更知识库内容",
"permission.des.manage": "可管理整个知识库数据和信息" "permission.des.manage": "可管理整个知识库数据和信息",
} "move.hint": "移动后,所选知识库/文件夹将继承新文件夹的权限设置,原先的权限设置失效。"
}

41
pnpm-lock.yaml generated
View File

@@ -220,6 +220,9 @@ importers:
pg: pg:
specifier: ^8.10.0 specifier: ^8.10.0
version: 8.12.0 version: 8.12.0
request-ip:
specifier: ^3.3.0
version: 3.3.0
tiktoken: tiktoken:
specifier: ^1.0.15 specifier: ^1.0.15
version: 1.0.15 version: 1.0.15
@@ -254,6 +257,9 @@ importers:
'@types/pg': '@types/pg':
specifier: ^8.6.6 specifier: ^8.6.6
version: 8.11.6 version: 8.11.6
'@types/request-ip':
specifier: ^0.0.37
version: 0.0.37
'@types/tunnel': '@types/tunnel':
specifier: ^0.0.4 specifier: ^0.0.4
version: 0.0.4 version: 0.0.4
@@ -562,12 +568,18 @@ importers:
specifier: ^4.3.5 specifier: ^4.3.5
version: 4.5.4(@types/react@18.3.1)(immer@9.0.21)(react@18.3.1) version: 4.5.4(@types/react@18.3.1)(immer@9.0.21)(react@18.3.1)
devDependencies: devDependencies:
'@faker-js/faker':
specifier: ^9.0.3
version: 9.0.3
'@shelf/jest-mongodb': '@shelf/jest-mongodb':
specifier: ^4.3.2 specifier: ^4.3.2
version: 4.3.2(jest-environment-node@29.7.0)(mongodb@6.9.0(socks@2.8.3)) version: 4.3.2(jest-environment-node@29.7.0)(mongodb@6.9.0(socks@2.8.3))
'@svgr/webpack': '@svgr/webpack':
specifier: ^6.5.1 specifier: ^6.5.1
version: 6.5.1 version: 6.5.1
'@types/faker':
specifier: ^6.6.9
version: 6.6.9
'@types/formidable': '@types/formidable':
specifier: ^2.0.5 specifier: ^2.0.5
version: 2.0.6 version: 2.0.6
@@ -2281,6 +2293,10 @@ packages:
resolution: {integrity: sha512-gMsVel9D7f2HLkBma9VbtzZRehRogVRfbr++f06nL2vnCGCNlzOD+/MUov/F4p8myyAHspEhVobgjpX64q5m6A==} resolution: {integrity: sha512-gMsVel9D7f2HLkBma9VbtzZRehRogVRfbr++f06nL2vnCGCNlzOD+/MUov/F4p8myyAHspEhVobgjpX64q5m6A==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
'@faker-js/faker@9.0.3':
resolution: {integrity: sha512-lWrrK4QNlFSU+13PL9jMbMKLJYXDFu3tQfayBsMXX7KL/GiQeqfB1CzHkqD5UHBUtPAuPo6XwGbMFNdVMZObRA==}
engines: {node: '>=18.0.0', npm: '>=9.0.0'}
'@fastify/accept-negotiator@1.1.0': '@fastify/accept-negotiator@1.1.0':
resolution: {integrity: sha512-OIHZrb2ImZ7XG85HXOONLcJWGosv7sIvM2ifAPQVhg9Lv7qdmMBNVaai4QTdyuaqbKM5eO6sLSQOYI7wEQeCJQ==} resolution: {integrity: sha512-OIHZrb2ImZ7XG85HXOONLcJWGosv7sIvM2ifAPQVhg9Lv7qdmMBNVaai4QTdyuaqbKM5eO6sLSQOYI7wEQeCJQ==}
engines: {node: '>=14'} engines: {node: '>=14'}
@@ -3326,6 +3342,10 @@ packages:
'@types/express@4.17.21': '@types/express@4.17.21':
resolution: {integrity: sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==} resolution: {integrity: sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==}
'@types/faker@6.6.9':
resolution: {integrity: sha512-Y9YYm5L//8ooiiknO++4Gr539zzdI0j3aXnOBjo1Vk+kTvffY10GuE2wn78AFPECwZ5MYGTjiDVw1naLLdDimw==}
deprecated: This is a stub types definition. faker provides its own type definitions, so you do not need this installed.
'@types/formidable@2.0.6': '@types/formidable@2.0.6':
resolution: {integrity: sha512-L4HcrA05IgQyNYJj6kItuIkXrInJvsXTPC5B1i64FggWKKqSL+4hgt7asiSNva75AoLQjq29oPxFfU4GAQ6Z2w==} resolution: {integrity: sha512-L4HcrA05IgQyNYJj6kItuIkXrInJvsXTPC5B1i64FggWKKqSL+4hgt7asiSNva75AoLQjq29oPxFfU4GAQ6Z2w==}
@@ -3449,6 +3469,9 @@ packages:
'@types/react-syntax-highlighter@15.5.13': '@types/react-syntax-highlighter@15.5.13':
resolution: {integrity: sha512-uLGJ87j6Sz8UaBAooU0T6lWJ0dBmjZgN1PZTrj05TNql2/XpC6+4HhMT5syIdFUUt+FASfCeLLv4kBygNU+8qA==} resolution: {integrity: sha512-uLGJ87j6Sz8UaBAooU0T6lWJ0dBmjZgN1PZTrj05TNql2/XpC6+4HhMT5syIdFUUt+FASfCeLLv4kBygNU+8qA==}
'@types/react@18.3.0':
resolution: {integrity: sha512-DiUcKjzE6soLyln8NNZmyhcQjVv+WsUIFSqetMN0p8927OztKT4VTfFTqsbAi5oAGIcgOmOajlfBqyptDDjZRw==}
'@types/react@18.3.1': '@types/react@18.3.1':
resolution: {integrity: sha512-V0kuGBX3+prX+DQ/7r2qsv1NsdfnCLnTgnRJ1pYnxykBhGMz+qj+box5lq7XsO5mtZsBqpjwwTu/7wszPfMBcw==} resolution: {integrity: sha512-V0kuGBX3+prX+DQ/7r2qsv1NsdfnCLnTgnRJ1pYnxykBhGMz+qj+box5lq7XsO5mtZsBqpjwwTu/7wszPfMBcw==}
@@ -5184,6 +5207,9 @@ packages:
resolution: {integrity: sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==} resolution: {integrity: sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==}
engines: {node: '>=4'} engines: {node: '>=4'}
faker@6.6.6:
resolution: {integrity: sha512-9tCqYEDHI5RYFQigXFwF1hnCwcWCOJl/hmll0lr5D2Ljjb0o4wphb69wikeJDz5qCEzXCoPvG6ss5SDP6IfOdg==}
fast-content-type-parse@1.1.0: fast-content-type-parse@1.1.0:
resolution: {integrity: sha512-fBHHqSTFLVnR61C+gltJuE5GkVQMV0S2nqUO8TJ+5Z3qAKG8vAx4FKai1s5jq/inV1+sREynIWSuQ6HgoSXpDQ==} resolution: {integrity: sha512-fBHHqSTFLVnR61C+gltJuE5GkVQMV0S2nqUO8TJ+5Z3qAKG8vAx4FKai1s5jq/inV1+sREynIWSuQ6HgoSXpDQ==}
@@ -11112,6 +11138,8 @@ snapshots:
'@eslint/js@8.56.0': {} '@eslint/js@8.56.0': {}
'@faker-js/faker@9.0.3': {}
'@fastify/accept-negotiator@1.1.0': {} '@fastify/accept-negotiator@1.1.0': {}
'@fastify/ajv-compiler@3.6.0': '@fastify/ajv-compiler@3.6.0':
@@ -12336,6 +12364,10 @@ snapshots:
'@types/qs': 6.9.15 '@types/qs': 6.9.15
'@types/serve-static': 1.15.7 '@types/serve-static': 1.15.7
'@types/faker@6.6.9':
dependencies:
faker: 6.6.6
'@types/formidable@2.0.6': '@types/formidable@2.0.6':
dependencies: dependencies:
'@types/node': 20.14.11 '@types/node': 20.14.11
@@ -12464,7 +12496,7 @@ snapshots:
'@types/react-redux@7.1.33': '@types/react-redux@7.1.33':
dependencies: dependencies:
'@types/hoist-non-react-statics': 3.3.5 '@types/hoist-non-react-statics': 3.3.5
'@types/react': 18.3.1 '@types/react': 18.3.0
hoist-non-react-statics: 3.3.2 hoist-non-react-statics: 3.3.2
redux: 4.2.1 redux: 4.2.1
@@ -12472,6 +12504,11 @@ snapshots:
dependencies: dependencies:
'@types/react': 18.3.1 '@types/react': 18.3.1
'@types/react@18.3.0':
dependencies:
'@types/prop-types': 15.7.12
csstype: 3.1.3
'@types/react@18.3.1': '@types/react@18.3.1':
dependencies: dependencies:
'@types/prop-types': 15.7.12 '@types/prop-types': 15.7.12
@@ -14679,6 +14716,8 @@ snapshots:
iconv-lite: 0.4.24 iconv-lite: 0.4.24
tmp: 0.0.33 tmp: 0.0.33
faker@6.6.6: {}
fast-content-type-parse@1.1.0: {} fast-content-type-parse@1.1.0: {}
fast-decode-uri-component@1.0.1: {} fast-decode-uri-component@1.0.1: {}

View File

@@ -37,5 +37,8 @@ FE_DOMAIN=http://localhost:3000
LOG_LEVEL=debug LOG_LEVEL=debug
STORE_LOG_LEVEL=warn STORE_LOG_LEVEL=warn
# 安全配置
# 工作流最大运行次数,避免极端的死循环情况 # 工作流最大运行次数,避免极端的死循环情况
WORKFLOW_MAX_RUN_TIMES=500 WORKFLOW_MAX_RUN_TIMES=500
# 循环最大运行次数,避免极端的死循环情况
WORKFLOW_MAX_LOOP_TIMES=50

View File

@@ -34,12 +34,7 @@ const config = {
// coverageProvider: "babel", // coverageProvider: "babel",
// A list of reporter names that Jest uses when writing coverage reports // A list of reporter names that Jest uses when writing coverage reports
// coverageReporters: [ coverageReporters: ['json', 'text', 'lcov', 'clover'],
// "json",
// "text",
// "lcov",
// "clover"
// ],
// An object that configures minimum threshold enforcement for coverage results // An object that configures minimum threshold enforcement for coverage results
// coverageThreshold: undefined, // coverageThreshold: undefined,
@@ -163,7 +158,7 @@ const config = {
// testRunner: "jest-circus/runner", // testRunner: "jest-circus/runner",
// A map from regular expressions to paths to transformers // A map from regular expressions to paths to transformers
// transform: undefined, transform: {},
// An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation
transformIgnorePatterns: [`/node_modules/(?!${esModules})`] transformIgnorePatterns: [`/node_modules/(?!${esModules})`]

View File

@@ -1,6 +1,6 @@
{ {
"name": "app", "name": "app",
"version": "4.8.11", "version": "4.8.12",
"private": false, "private": false,
"scripts": { "scripts": {
"dev": "next dev", "dev": "next dev",
@@ -71,8 +71,10 @@
"zustand": "^4.3.5" "zustand": "^4.3.5"
}, },
"devDependencies": { "devDependencies": {
"@faker-js/faker": "^9.0.3",
"@shelf/jest-mongodb": "^4.3.2", "@shelf/jest-mongodb": "^4.3.2",
"@svgr/webpack": "^6.5.1", "@svgr/webpack": "^6.5.1",
"@types/faker": "^6.6.9",
"@types/formidable": "^2.0.5", "@types/formidable": "^2.0.5",
"@types/js-yaml": "^4.0.9", "@types/js-yaml": "^4.0.9",
"@types/jsonwebtoken": "^9.0.3", "@types/jsonwebtoken": "^9.0.3",

View File

@@ -1,4 +1,4 @@
import React, { useCallback, useState } from 'react'; import React, { useState } from 'react';
import MyModal from '@fastgpt/web/components/common/MyModal'; import MyModal from '@fastgpt/web/components/common/MyModal';
import { useTranslation } from 'next-i18next'; import { useTranslation } from 'next-i18next';
import { Box, Button, Flex, ModalBody, ModalFooter } from '@chakra-ui/react'; import { Box, Button, Flex, ModalBody, ModalFooter } from '@chakra-ui/react';
@@ -11,6 +11,7 @@ import { useMemoizedFn, useMount } from 'ahooks';
import MyIcon from '@fastgpt/web/components/common/Icon'; import MyIcon from '@fastgpt/web/components/common/Icon';
import { FolderIcon } from '@fastgpt/global/common/file/image/constants'; import { FolderIcon } from '@fastgpt/global/common/file/image/constants';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest'; import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import LightTip from '@fastgpt/web/components/common/LightTip';
type FolderItemType = { type FolderItemType = {
id: string; id: string;
@@ -27,9 +28,10 @@ type Props = {
server: (e: GetResourceFolderListProps) => Promise<GetResourceFolderListItemResponse[]>; server: (e: GetResourceFolderListProps) => Promise<GetResourceFolderListItemResponse[]>;
onConfirm: (id: ParentIdType) => Promise<any>; onConfirm: (id: ParentIdType) => Promise<any>;
onClose: () => void; onClose: () => void;
moveHint?: string;
}; };
const MoveModal = ({ moveResourceId, title, server, onConfirm, onClose }: Props) => { const MoveModal = ({ moveResourceId, title, server, onConfirm, onClose, moveHint }: Props) => {
const { t } = useTranslation(); const { t } = useTranslation();
const [selectedId, setSelectedId] = React.useState<string>(); const [selectedId, setSelectedId] = React.useState<string>();
const [requestingIdList, setRequestingIdList] = useState<ParentIdType[]>([]); const [requestingIdList, setRequestingIdList] = useState<ParentIdType[]>([]);
@@ -170,6 +172,11 @@ const MoveModal = ({ moveResourceId, title, server, onConfirm, onClose }: Props)
onClose={onClose} onClose={onClose}
> >
<ModalBody flex={'1 0 0'} overflow={'auto'} minH={'400px'}> <ModalBody flex={'1 0 0'} overflow={'auto'} minH={'400px'}>
{moveHint && (
<Box mb={1}>
<LightTip text={moveHint} />
</Box>
)}
<RenderList list={folderList} /> <RenderList list={folderList} />
</ModalBody> </ModalBody>
<ModalFooter> <ModalFooter>

View File

@@ -1,5 +1,4 @@
import { Box, Button, Flex, HStack } from '@chakra-ui/react'; import { Box, Button, Flex, HStack } from '@chakra-ui/react';
import { useToast } from '@fastgpt/web/hooks/useToast';
import React from 'react'; import React from 'react';
import MyIcon from '@fastgpt/web/components/common/Icon'; import MyIcon from '@fastgpt/web/components/common/Icon';
import { FolderIcon } from '@fastgpt/global/common/file/image/constants'; import { FolderIcon } from '@fastgpt/global/common/file/image/constants';
@@ -40,7 +39,7 @@ const FolderSlideCard = ({
deleteTip: string; deleteTip: string;
onDelete: () => void; onDelete: () => void;
defaultPer: { defaultPer?: {
value: PermissionValueType; value: PermissionValueType;
defaultValue: PermissionValueType; defaultValue: PermissionValueType;
onChange: (v: PermissionValueType) => Promise<any>; onChange: (v: PermissionValueType) => Promise<any>;
@@ -54,7 +53,6 @@ const FolderSlideCard = ({
}) => { }) => {
const { t } = useTranslation(); const { t } = useTranslation();
const { feConfigs } = useSystemStore(); const { feConfigs } = useSystemStore();
const { toast } = useToast();
const { ConfirmModal, openConfirm } = useConfirm({ const { ConfirmModal, openConfirm } = useConfirm({
type: 'delete', type: 'delete',
@@ -136,7 +134,7 @@ const FolderSlideCard = ({
</Box> </Box>
)} )}
{managePer.permission.hasManagePer && ( {managePer.permission.hasManagePer && !!defaultPer && (
<Box mt={5}> <Box mt={5}>
<Box fontSize={'sm'} color={'myGray.500'}> <Box fontSize={'sm'} color={'myGray.500'}>
{t('common:permission.Default permission')} {t('common:permission.Default permission')}

View File

@@ -33,79 +33,6 @@ enum CronJobTypeEnum {
} }
type CronType = 'month' | 'week' | 'day' | 'interval'; type CronType = 'month' | 'week' | 'day' | 'interval';
const get24HoursOptions = () => {
return Array.from({ length: 24 }, (_, i) => ({
label: `${i < 10 ? '0' : ''}${i}:00`,
value: i
}));
};
const getRoute = (i: number) => {
switch (i) {
case 0:
return 'app:week.Sunday';
case 1:
return 'app:week.Monday';
case 2:
return 'app:week.Tuesday';
case 3:
return 'app:week.Wednesday';
case 4:
return 'app:week.Thursday';
case 5:
return 'app:week.Friday';
case 6:
return 'app:week.Saturday';
default:
return 'app:week.Sunday';
}
};
const getWeekOptions = () => {
return Array.from({ length: 7 }, (_, i) => {
return {
label: i18nT(getRoute(i)),
value: i,
children: get24HoursOptions()
};
});
};
const getMonthOptions = () => {
return Array.from({ length: 28 }, (_, i) => ({
label: `${i + 1}` + i18nT('app:month.unit'),
value: i,
children: get24HoursOptions()
}));
};
const getInterValOptions = () => {
// 每n小时
return [
{
label: i18nT('app:interval.per_hour'),
value: 1
},
{
label: i18nT('app:interval.2_hours'),
value: 2
},
{
label: i18nT('app:interval.3_hours'),
value: 3
},
{
label: i18nT('app:interval.4_hours'),
value: 4
},
{
label: i18nT('app:interval.6_hours'),
value: 6
},
{
label: i18nT('app:interval.12_hours'),
value: 12
}
];
};
const defaultValue = ['day', 0, 0]; const defaultValue = ['day', 0, 0];
const defaultCronString = '0 0 * * *'; const defaultCronString = '0 0 * * *';
@@ -125,6 +52,81 @@ const ScheduledTriggerConfig = ({
const cronString = value?.cronString; const cronString = value?.cronString;
const defaultPrompt = value?.defaultPrompt; const defaultPrompt = value?.defaultPrompt;
const get24HoursOptions = () => {
return Array.from({ length: 24 }, (_, i) => ({
label: `${i < 10 ? '0' : ''}${i}:00`,
value: i
}));
};
const getRoute = (i: number) => {
const { t } = useTranslation();
switch (i) {
case 0:
return t('app:week.Sunday');
case 1:
return t('app:week.Monday');
case 2:
return t('app:week.Tuesday');
case 3:
return t('app:week.Wednesday');
case 4:
return t('app:week.Thursday');
case 5:
return t('app:week.Friday');
case 6:
return t('app:week.Saturday');
default:
return t('app:week.Sunday');
}
};
const getWeekOptions = () => {
return Array.from({ length: 7 }, (_, i) => {
return {
label: getRoute(i),
value: i,
children: get24HoursOptions()
};
});
};
const getMonthOptions = () => {
return Array.from({ length: 28 }, (_, i) => ({
label: `${i + 1}` + t('app:month.unit'),
value: i,
children: get24HoursOptions()
}));
};
const getInterValOptions = () => {
// 每n小时
return [
{
label: t('app:interval.per_hour'),
value: 1
},
{
label: t('app:interval.2_hours'),
value: 2
},
{
label: t('app:interval.3_hours'),
value: 3
},
{
label: t('app:interval.4_hours'),
value: 4
},
{
label: t('app:interval.6_hours'),
value: 6
},
{
label: t('app:interval.12_hours'),
value: 12
}
];
};
const cronSelectList = useRef<MultipleSelectProps['list']>([ const cronSelectList = useRef<MultipleSelectProps['list']>([
{ {
label: t('app:cron.every_day'), label: t('app:cron.every_day'),

View File

@@ -1,11 +1,9 @@
import React from 'react'; import React from 'react';
import MyModal from '@fastgpt/web/components/common/MyModal'; import MyModal from '@fastgpt/web/components/common/MyModal';
import { useTranslation } from 'next-i18next'; import { useTranslation } from 'next-i18next';
import { PermissionValueType } from '@fastgpt/global/support/permission/type';
import CollaboratorContextProvider, { MemberManagerInputPropsType } from '../MemberManager/context'; import CollaboratorContextProvider, { MemberManagerInputPropsType } from '../MemberManager/context';
import { Box, Button, Flex, HStack, ModalBody, useDisclosure } from '@chakra-ui/react'; import { Box, Button, Flex, HStack, ModalBody, useDisclosure } from '@chakra-ui/react';
import Avatar from '@fastgpt/web/components/common/Avatar'; import Avatar from '@fastgpt/web/components/common/Avatar';
import DefaultPermissionList from '../DefaultPerList';
import MyIcon from '@fastgpt/web/components/common/Icon'; import MyIcon from '@fastgpt/web/components/common/Icon';
import ResumeInherit from '../ResumeInheritText'; import ResumeInherit from '../ResumeInheritText';
import { ChangeOwnerModal } from '../ChangeOwnerModal'; import { ChangeOwnerModal } from '../ChangeOwnerModal';
@@ -14,11 +12,6 @@ export type ConfigPerModalProps = {
avatar?: string; avatar?: string;
name: string; name: string;
defaultPer: {
value: PermissionValueType;
defaultValue: PermissionValueType;
onChange: (v: PermissionValueType) => Promise<any>;
};
managePer: MemberManagerInputPropsType; managePer: MemberManagerInputPropsType;
isInheritPermission?: boolean; isInheritPermission?: boolean;
resumeInheritPermission?: () => void; resumeInheritPermission?: () => void;
@@ -30,7 +23,6 @@ export type ConfigPerModalProps = {
const ConfigPerModal = ({ const ConfigPerModal = ({
avatar, avatar,
name, name,
defaultPer,
managePer, managePer,
isInheritPermission, isInheritPermission,
resumeInheritPermission, resumeInheritPermission,
@@ -66,17 +58,6 @@ const ConfigPerModal = ({
<ResumeInherit onResume={resumeInheritPermission} /> <ResumeInherit onResume={resumeInheritPermission} />
</Box> </Box>
)} )}
<Box mt={5}>
<Box fontSize={'sm'}>{t('common:permission.Default permission')}</Box>
<DefaultPermissionList
mt="1"
per={defaultPer.value}
defaultPer={defaultPer.defaultValue}
isInheritPermission={isInheritPermission}
onChange={(v) => defaultPer.onChange(v)}
hasParent={hasParent}
/>
</Box>
<Box mt={4}> <Box mt={4}>
<CollaboratorContextProvider <CollaboratorContextProvider
{...managePer} {...managePer}

View File

@@ -1,35 +1,22 @@
import React, { useMemo } from 'react'; import React from 'react';
import { PermissionTypeEnum, PermissionTypeMap } from '@fastgpt/global/support/permission/constant'; import { PermissionTypeMap } from '@fastgpt/global/support/permission/constant';
import { Box, StackProps, HStack } from '@chakra-ui/react'; import { Box, StackProps, HStack } from '@chakra-ui/react';
import MyIcon from '@fastgpt/web/components/common/Icon'; import MyIcon from '@fastgpt/web/components/common/Icon';
import { useTranslation } from 'next-i18next'; import { useTranslation } from 'next-i18next';
import { PermissionValueType } from '@fastgpt/global/support/permission/type';
import { Permission } from '@fastgpt/global/support/permission/controller';
const PermissionIconText = ({ const PermissionIconText = ({
permission,
defaultPermission,
w = '1rem', w = '1rem',
fontSize = 'mini', fontSize = 'mini',
iconColor = 'myGray.500', iconColor = 'myGray.500',
private: Private = false,
...props ...props
}: { }: {
permission?: `${PermissionTypeEnum}`; private?: boolean;
defaultPermission?: PermissionValueType;
iconColor?: string; iconColor?: string;
} & StackProps) => { } & StackProps) => {
const { t } = useTranslation(); const { t } = useTranslation();
const per = useMemo(() => { const per = Private ? 'private' : 'public';
if (permission) return permission;
if (defaultPermission !== undefined) {
const Per = new Permission({ per: defaultPermission });
if (Per.hasWritePer) return PermissionTypeEnum.publicWrite;
if (Per.hasReadPer) return PermissionTypeEnum.publicRead;
return PermissionTypeEnum.clbPrivate;
}
return 'private';
}, [defaultPermission, permission]);
return PermissionTypeMap[per] ? ( return PermissionTypeMap[per] ? (
<HStack spacing={1} fontSize={fontSize} {...props}> <HStack spacing={1} fontSize={fontSize} {...props}>

View File

@@ -2,12 +2,11 @@ import {
Flex, Flex,
Box, Box,
ModalBody, ModalBody,
InputGroup,
InputLeftElement,
Input,
Checkbox, Checkbox,
ModalFooter, ModalFooter,
Button Button,
Grid,
HStack
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import MyModal from '@fastgpt/web/components/common/MyModal'; import MyModal from '@fastgpt/web/components/common/MyModal';
import MyIcon from '@fastgpt/web/components/common/Icon'; import MyIcon from '@fastgpt/web/components/common/Icon';
@@ -18,63 +17,76 @@ import PermissionSelect from './PermissionSelect';
import PermissionTags from './PermissionTags'; import PermissionTags from './PermissionTags';
import { CollaboratorContext } from './context'; import { CollaboratorContext } from './context';
import { useUserStore } from '@/web/support/user/useUserStore'; import { useUserStore } from '@/web/support/user/useUserStore';
import MyBox from '@fastgpt/web/components/common/MyBox';
import { ChevronDownIcon } from '@chakra-ui/icons'; import { ChevronDownIcon } from '@chakra-ui/icons';
import Avatar from '@fastgpt/web/components/common/Avatar'; import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { useRequest, useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { useTranslation } from 'next-i18next'; import { useTranslation } from 'next-i18next';
import SearchInput from '@fastgpt/web/components/common/Input/SearchInput';
import { DefaultGroupName } from '@fastgpt/global/support/user/team/group/constant';
export type AddModalPropsType = { export type AddModalPropsType = {
onClose: () => void; onClose: () => void;
mode?: 'member' | 'all';
}; };
function AddMemberModal({ onClose }: AddModalPropsType) { function AddMemberModal({ onClose, mode = 'member' }: AddModalPropsType) {
const { t } = useTranslation(); const { t } = useTranslation();
const { userInfo, loadAndGetTeamMembers } = useUserStore(); const { userInfo, loadAndGetTeamMembers, loadAndGetGroups, myGroups } = useUserStore();
const { permissionList, collaboratorList, onUpdateCollaborators, getPerLabelList } = const { permissionList, collaboratorList, onUpdateCollaborators, getPerLabelList, permission } =
useContextSelector(CollaboratorContext, (v) => v); useContextSelector(CollaboratorContext, (v) => v);
const [searchText, setSearchText] = useState<string>(''); const [searchText, setSearchText] = useState<string>('');
const { data: members = [], loading: loadingMembers } = useRequest2( const { data: [members = [], groups = []] = [], loading: loadingMembersAndGroups } = useRequest2(
async () => { async () => {
if (!userInfo?.team?.teamId) return []; if (!userInfo?.team?.teamId) return [[], []];
const members = await loadAndGetTeamMembers(true); return await Promise.all([loadAndGetTeamMembers(true), loadAndGetGroups(true)]);
return members;
}, },
{ {
manual: false, manual: false,
refreshDeps: [userInfo?.team?.teamId] refreshDeps: [userInfo?.team?.teamId]
} }
); );
const filterMembers = useMemo(() => { const filterMembers = useMemo(() => {
return members.filter((item) => { return members.filter((item) => {
// if (item.permission.isOwner) return false;
if (item.tmbId === userInfo?.team?.tmbId) return false; if (item.tmbId === userInfo?.team?.tmbId) return false;
if (!searchText) return true; if (!searchText) return true;
return item.memberName.includes(searchText); return item.memberName.includes(searchText);
}); });
}, [members, searchText, userInfo?.team?.tmbId]); }, [members, searchText, userInfo?.team?.tmbId]);
const filterGroups = useMemo(() => {
if (mode !== 'all') return [];
return groups.filter((item) => {
if (permission.isOwner) return true; // owner can see all groups
if (myGroups.find((i) => String(i._id) === String(item._id))) return false;
if (!searchText) return true;
return item.name.includes(searchText);
});
}, [groups, searchText, myGroups, mode, permission]);
const [selectedMemberIdList, setSelectedMembers] = useState<string[]>([]); const [selectedMemberIdList, setSelectedMembers] = useState<string[]>([]);
const [selectedGroupIdList, setSelectedGroupIdList] = useState<string[]>([]);
const [selectedPermission, setSelectedPermission] = useState(permissionList['read'].value); const [selectedPermission, setSelectedPermission] = useState(permissionList['read'].value);
const perLabel = useMemo(() => { const perLabel = useMemo(() => {
return getPerLabelList(selectedPermission).join('、'); return getPerLabelList(selectedPermission).join('、');
}, [getPerLabelList, selectedPermission]); }, [getPerLabelList, selectedPermission]);
const { mutate: onConfirm, isLoading: isUpdating } = useRequest({ const { runAsync: onConfirm, loading: isUpdating } = useRequest2(
mutationFn: () => { () =>
return onUpdateCollaborators({ onUpdateCollaborators({
members: selectedMemberIdList, members: selectedMemberIdList,
groups: selectedGroupIdList,
permission: selectedPermission permission: selectedPermission
}); }),
}, {
successToast: t('common:common.Add Success'), successToast: t('common:common.Add Success'),
errorToast: 'Error', errorToast: 'Error',
onSuccess() { onSuccess() {
onClose(); onClose();
}
} }
}); );
return ( return (
<MyModal <MyModal
@@ -83,17 +95,15 @@ function AddMemberModal({ onClose }: AddModalPropsType) {
iconSrc="modal/AddClb" iconSrc="modal/AddClb"
title={t('user:team.add_collaborator')} title={t('user:team.add_collaborator')}
minW="800px" minW="800px"
isCentered
isLoading={loadingMembersAndGroups}
> >
<ModalBody> <ModalBody>
<MyBox <Grid
isLoading={loadingMembers}
display={'grid'}
minH="400px"
border="1px solid" border="1px solid"
borderColor="myGray.200" borderColor="myGray.200"
borderRadius="0.5rem" borderRadius="0.5rem"
gridTemplateColumns="55% 45%" gridTemplateColumns="1fr 1fr"
fontSize={'sm'}
> >
<Flex <Flex
flexDirection="column" flexDirection="column"
@@ -102,17 +112,53 @@ function AddMemberModal({ onClose }: AddModalPropsType) {
p="4" p="4"
minH="200px" minH="200px"
> >
<InputGroup alignItems="center" size="sm"> <SearchInput
<InputLeftElement> placeholder={t('user:search_user')}
<MyIcon name="common/searchLight" w="16px" color={'myGray.500'} /> bgColor="myGray.50"
</InputLeftElement> onChange={(e) => setSearchText(e.target.value)}
<Input />
placeholder={t('user:search_user')}
bgColor="myGray.50" <Flex flexDirection="column" mt="2" overflow={'auto'} maxH="400px">
onChange={(e) => setSearchText(e.target.value)} {filterGroups.map((group) => {
/> const onChange = () => {
</InputGroup> if (selectedGroupIdList.includes(group._id)) {
<Flex flexDirection="column" mt="2"> setSelectedGroupIdList(selectedGroupIdList.filter((v) => v !== group._id));
} else {
setSelectedGroupIdList([...selectedGroupIdList, group._id]);
}
};
const collaborator = collaboratorList.find((v) => v.groupId === group._id);
return (
<HStack
justifyContent="space-between"
key={group._id}
py="2"
px="3"
borderRadius="sm"
alignItems="center"
_hover={{
bgColor: 'myGray.50',
cursor: 'pointer',
...(!selectedGroupIdList.includes(group._id)
? { svg: { color: 'myGray.50' } }
: {})
}}
onClick={onChange}
>
<Checkbox
isChecked={selectedGroupIdList.includes(group._id)}
icon={<MyIcon name={'common/check'} w={'12px'} />}
/>
<MyAvatar src={group.avatar} w="1.5rem" borderRadius={'50%'} />
<Box ml="2" w="full">
{group.name === DefaultGroupName ? userInfo?.team.teamName : group.name}
</Box>
{!!collaborator && (
<PermissionTags permission={collaborator.permission.value} />
)}
</HStack>
);
})}
{filterMembers.map((member) => { {filterMembers.map((member) => {
const onChange = () => { const onChange = () => {
if (selectedMemberIdList.includes(member.tmbId)) { if (selectedMemberIdList.includes(member.tmbId)) {
@@ -123,10 +169,10 @@ function AddMemberModal({ onClose }: AddModalPropsType) {
}; };
const collaborator = collaboratorList.find((v) => v.tmbId === member.tmbId); const collaborator = collaboratorList.find((v) => v.tmbId === member.tmbId);
return ( return (
<Flex <HStack
justifyContent="space-between"
key={member.tmbId} key={member.tmbId}
mt="1" py="2"
py="1"
px="3" px="3"
borderRadius="sm" borderRadius="sm"
alignItems="center" alignItems="center"
@@ -137,51 +183,87 @@ function AddMemberModal({ onClose }: AddModalPropsType) {
? { svg: { color: 'myGray.50' } } ? { svg: { color: 'myGray.50' } }
: {}) : {})
}} }}
onClick={onChange}
> >
<Checkbox <Checkbox
mr="3"
isChecked={selectedMemberIdList.includes(member.tmbId)} isChecked={selectedMemberIdList.includes(member.tmbId)}
icon={<MyIcon name={'common/check'} w={'12px'} />} icon={<MyIcon name={'common/check'} w={'12px'} />}
onChange={onChange}
/> />
<Flex <MyAvatar src={member.avatar} w="1.5rem" borderRadius={'50%'} />
flexDirection="row" <Box w="full" ml="2">
onClick={onChange} {member.memberName}
w="full" </Box>
justifyContent="space-between" {!!collaborator && (
> <PermissionTags permission={collaborator.permission.value} />
<Flex flexDirection="row" alignItems="center"> )}
<MyAvatar src={member.avatar} w="32px" /> </HStack>
<Box ml="2">{member.memberName}</Box>
</Flex>
{!!collaborator && (
<PermissionTags permission={collaborator.permission.value} />
)}
</Flex>
</Flex>
); );
})} })}
</Flex> </Flex>
</Flex> </Flex>
<Flex p="4" flexDirection="column"> <Flex p="4" flexDirection="column">
<Box> <Box>
{t('user:has_chosen') + ': '}+ {selectedMemberIdList.length} {t('user:has_chosen') + ': '}{' '}
{selectedMemberIdList.length + selectedGroupIdList.length}
</Box> </Box>
<Flex flexDirection="column" mt="2"> <Flex flexDirection="column" mt="2" overflow={'auto'} maxH="400px">
{selectedGroupIdList.map((groupId) => {
const onChange = () => {
if (selectedGroupIdList.includes(groupId)) {
setSelectedGroupIdList(selectedGroupIdList.filter((v) => v !== groupId));
} else {
setSelectedGroupIdList([...selectedGroupIdList, groupId]);
}
};
const group = groups.find((v) => String(v._id) === groupId);
return (
<HStack
justifyContent="space-between"
key={groupId}
py="2"
px="3"
borderRadius="sm"
alignItems="center"
_hover={{
bgColor: 'myGray.50',
cursor: 'pointer',
...(!selectedGroupIdList.includes(groupId)
? { svg: { color: 'myGray.50' } }
: {})
}}
onClick={onChange}
>
<MyAvatar src={group?.avatar} w="1.5rem" borderRadius={'50%'} />
<Box w="full" ml="2">
{group?.name === DefaultGroupName ? userInfo?.team.teamName : group?.name}
</Box>
<MyIcon
name="common/closeLight"
w="16px"
cursor={'pointer'}
_hover={{
color: 'red.600'
}}
/>
</HStack>
);
})}
{selectedMemberIdList.map((tmbId) => { {selectedMemberIdList.map((tmbId) => {
const member = filterMembers.find((v) => v.tmbId === tmbId); const member = filterMembers.find((v) => v.tmbId === tmbId);
return member ? ( return member ? (
<Flex <HStack
justifyContent="space-between"
key={tmbId} key={tmbId}
alignItems="center" alignItems="center"
justifyContent="space-between"
py="2" py="2"
px={3} px={3}
borderRadius={'md'} borderRadius={'md'}
_hover={{ bg: 'myGray.50' }} _hover={{ bg: 'myGray.50' }}
_notLast={{ mb: 2 }} onClick={() =>
setSelectedMembers(selectedMemberIdList.filter((v) => v !== tmbId))
}
> >
<Avatar src={member.avatar} w="24px" /> <MyAvatar src={member.avatar} w="1.5rem" borderRadius="50%" />
<Box w="full" ml={2}> <Box w="full" ml={2}>
{member.memberName} {member.memberName}
</Box> </Box>
@@ -192,16 +274,13 @@ function AddMemberModal({ onClose }: AddModalPropsType) {
_hover={{ _hover={{
color: 'red.600' color: 'red.600'
}} }}
onClick={() =>
setSelectedMembers(selectedMemberIdList.filter((v) => v !== tmbId))
}
/> />
</Flex> </HStack>
) : null; ) : null;
})} })}
</Flex> </Flex>
</Flex> </Flex>
</MyBox> </Grid>
</ModalBody> </ModalBody>
<ModalFooter> <ModalFooter>
<PermissionSelect <PermissionSelect

View File

@@ -8,11 +8,12 @@ import Avatar from '@fastgpt/web/components/common/Avatar';
import { CollaboratorContext } from './context'; import { CollaboratorContext } from './context';
import MyIcon from '@fastgpt/web/components/common/Icon'; import MyIcon from '@fastgpt/web/components/common/Icon';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest'; import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { PermissionValueType } from '@fastgpt/global/support/permission/type';
import { useUserStore } from '@/web/support/user/useUserStore'; import { useUserStore } from '@/web/support/user/useUserStore';
import EmptyTip from '@fastgpt/web/components/common/EmptyTip'; import EmptyTip from '@fastgpt/web/components/common/EmptyTip';
import Loading from '@fastgpt/web/components/common/MyLoading'; import Loading from '@fastgpt/web/components/common/MyLoading';
import { useTranslation } from 'next-i18next'; import { useTranslation } from 'next-i18next';
import { DefaultGroupName } from '@fastgpt/global/support/user/team/group/constant';
import { RequireOnlyOne } from '@fastgpt/global/common/type/utils';
export type ManageModalProps = { export type ManageModalProps = {
onClose: () => void; onClose: () => void;
}; };
@@ -23,21 +24,12 @@ function ManageModal({ onClose }: ManageModalProps) {
const { permission, collaboratorList, onUpdateCollaborators, onDelOneCollaborator } = const { permission, collaboratorList, onUpdateCollaborators, onDelOneCollaborator } =
useContextSelector(CollaboratorContext, (v) => v); useContextSelector(CollaboratorContext, (v) => v);
const { runAsync: onDelete, loading: isDeleting } = useRequest2((tmbId: string) => const { runAsync: onDelete, loading: isDeleting } = useRequest2(onDelOneCollaborator);
onDelOneCollaborator(tmbId)
);
const { runAsync: onUpdate, loading: isUpdating } = useRequest2( const { runAsync: onUpdate, loading: isUpdating } = useRequest2(onUpdateCollaborators, {
({ tmbId, per }: { tmbId: string; per: PermissionValueType }) => successToast: t('common.Update Success'),
onUpdateCollaborators({ errorToast: 'Error'
members: [tmbId], });
permission: per
}),
{
successToast: t('common.Update Success'),
errorToast: 'Error'
}
);
const loading = isDeleting || isUpdating; const loading = isDeleting || isUpdating;
@@ -74,7 +66,7 @@ function ManageModal({ onClose }: ManageModalProps) {
<Td border="none"> <Td border="none">
<Flex alignItems="center"> <Flex alignItems="center">
<Avatar src={item.avatar} w="24px" mr={2} /> <Avatar src={item.avatar} w="24px" mr={2} />
{item.name} {item.name === DefaultGroupName ? userInfo?.team.teamName : item.name}
</Flex> </Flex>
</Td> </Td>
<Td border="none"> <Td border="none">
@@ -89,14 +81,18 @@ function ManageModal({ onClose }: ManageModalProps) {
<MyIcon name={'edit'} w={'16px'} _hover={{ color: 'primary.600' }} /> <MyIcon name={'edit'} w={'16px'} _hover={{ color: 'primary.600' }} />
} }
value={item.permission.value} value={item.permission.value}
onChange={(per) => { onChange={(permission) => {
onUpdate({ onUpdate({
tmbId: item.tmbId, members: item.tmbId ? [item.tmbId] : undefined,
per groups: item.groupId ? [item.groupId] : undefined,
permission
}); });
}} }}
onDelete={() => { onDelete={() => {
onDelete(item.tmbId); onDelete({
tmbId: item.tmbId,
groupId: item.groupId
} as RequireOnlyOne<{ tmbId: string; groupId: string }>);
}} }}
/> />
)} )}

View File

@@ -6,11 +6,14 @@ import { CollaboratorContext } from './context';
import Tag, { TagProps } from '@fastgpt/web/components/common/Tag'; import Tag, { TagProps } from '@fastgpt/web/components/common/Tag';
import Avatar from '@fastgpt/web/components/common/Avatar'; import Avatar from '@fastgpt/web/components/common/Avatar';
import { useTranslation } from 'next-i18next'; import { useTranslation } from 'next-i18next';
import { DefaultGroupName } from '@fastgpt/global/support/user/team/group/constant';
import { useUserStore } from '@/web/support/user/useUserStore';
export type MemberListCardProps = BoxProps & { tagStyle?: Omit<TagProps, 'children'> }; export type MemberListCardProps = BoxProps & { tagStyle?: Omit<TagProps, 'children'> };
const MemberListCard = ({ tagStyle, ...props }: MemberListCardProps) => { const MemberListCard = ({ tagStyle, ...props }: MemberListCardProps) => {
const { t } = useTranslation(); const { t } = useTranslation();
const { userInfo } = useUserStore();
const { collaboratorList, isFetchingCollaborator } = useContextSelector( const { collaboratorList, isFetchingCollaborator } = useContextSelector(
CollaboratorContext, CollaboratorContext,
@@ -27,10 +30,15 @@ const MemberListCard = ({ tagStyle, ...props }: MemberListCardProps) => {
<Flex gap="2" flexWrap={'wrap'}> <Flex gap="2" flexWrap={'wrap'}>
{collaboratorList?.map((member) => { {collaboratorList?.map((member) => {
return ( return (
<Tag key={member.tmbId} type={'fill'} colorSchema="white" {...tagStyle}> <Tag
key={member.tmbId || member.groupId}
type={'fill'}
colorSchema="white"
{...tagStyle}
>
<Avatar src={member.avatar} w="1.25rem" /> <Avatar src={member.avatar} w="1.25rem" />
<Box fontSize={'sm'} ml={1}> <Box fontSize={'sm'} ml={1}>
{member.name} {member.name === DefaultGroupName ? userInfo?.team.teamName : member.name}
</Box> </Box>
</Tag> </Tag>
); );

View File

@@ -15,6 +15,7 @@ import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { useSystemStore } from '@/web/common/system/useSystemStore'; import { useSystemStore } from '@/web/common/system/useSystemStore';
import { useConfirm } from '@fastgpt/web/hooks/useConfirm'; import { useConfirm } from '@fastgpt/web/hooks/useConfirm';
import { useI18n } from '@/web/context/I18n'; import { useI18n } from '@/web/context/I18n';
import { RequireOnlyOne } from '@fastgpt/global/common/type/utils';
const AddMemberModal = dynamic(() => import('./AddMemberModal')); const AddMemberModal = dynamic(() => import('./AddMemberModal'));
const ManageModal = dynamic(() => import('./ManageModal')); const ManageModal = dynamic(() => import('./ManageModal'));
@@ -22,10 +23,12 @@ export type MemberManagerInputPropsType = {
permission: Permission; permission: Permission;
onGetCollaboratorList: () => Promise<CollaboratorItemType[]>; onGetCollaboratorList: () => Promise<CollaboratorItemType[]>;
permissionList: PermissionListType; permissionList: PermissionListType;
onUpdateCollaborators: (props: any) => any; // TODO: type. should be UpdatePermissionBody after app and dataset permission refactored onUpdateCollaborators: (props: UpdateClbPermissionProps) => Promise<any>;
onDelOneCollaborator: (tmbId: string) => any; onDelOneCollaborator: (props: RequireOnlyOne<{ tmbId: string; groupId: string }>) => Promise<any>;
refreshDeps?: any[]; refreshDeps?: any[];
mode?: 'member' | 'all';
}; };
export type MemberManagerPropsType = MemberManagerInputPropsType & { export type MemberManagerPropsType = MemberManagerInputPropsType & {
collaboratorList: CollaboratorItemType[]; collaboratorList: CollaboratorItemType[];
refetchCollaboratorList: () => void; refetchCollaboratorList: () => void;
@@ -72,7 +75,8 @@ const CollaboratorContextProvider = ({
refetchResource, refetchResource,
refreshDeps = [], refreshDeps = [],
isInheritPermission, isInheritPermission,
hasParent hasParent,
mode = 'member'
}: MemberManagerInputPropsType & { }: MemberManagerInputPropsType & {
children: (props: ChildrenProps) => ReactNode; children: (props: ChildrenProps) => ReactNode;
refetchResource?: () => void; refetchResource?: () => void;
@@ -83,8 +87,10 @@ const CollaboratorContextProvider = ({
await onUpdateCollaborators(props); await onUpdateCollaborators(props);
refetchCollaboratorList(); refetchCollaboratorList();
}; };
const onDelOneCollaboratorThen = async (tmbId: string) => { const onDelOneCollaboratorThen = async (
await onDelOneCollaborator(tmbId); props: RequireOnlyOne<{ tmbId: string; groupId: string }>
) => {
await onDelOneCollaborator(props);
refetchCollaboratorList(); refetchCollaboratorList();
}; };
@@ -197,6 +203,7 @@ const CollaboratorContextProvider = ({
onCloseAddMember(); onCloseAddMember();
refetchResource?.(); refetchResource?.();
}} }}
mode={mode}
/> />
)} )}
{isOpenManageModal && ( {isOpenManageModal && (

View File

@@ -1,14 +1,5 @@
import React, { useMemo, useState } from 'react'; import React, { useMemo, useState } from 'react';
import { import { Box, Checkbox, Flex, Grid, HStack } from '@chakra-ui/react';
Box,
Checkbox,
Flex,
Grid,
HStack,
Input,
InputGroup,
InputLeftElement
} from '@chakra-ui/react';
import MyIcon from '@fastgpt/web/components/common/Icon'; import MyIcon from '@fastgpt/web/components/common/Icon';
import Avatar from '@fastgpt/web/components/common/Avatar'; import Avatar from '@fastgpt/web/components/common/Avatar';
import { useTranslation } from 'next-i18next'; import { useTranslation } from 'next-i18next';
@@ -16,6 +7,7 @@ import { Control, Controller } from 'react-hook-form';
import { RequireAtLeastOne } from '@fastgpt/global/common/type/utils'; import { RequireAtLeastOne } from '@fastgpt/global/common/type/utils';
import { useUserStore } from '@/web/support/user/useUserStore'; import { useUserStore } from '@/web/support/user/useUserStore';
import { DefaultGroupName } from '@fastgpt/global/support/user/team/group/constant'; import { DefaultGroupName } from '@fastgpt/global/support/user/team/group/constant';
import SearchInput from '@fastgpt/web/components/common/Input/SearchInput';
type memberType = { type memberType = {
type: 'member'; type: 'member';
@@ -120,19 +112,14 @@ function SelectMember({
h={'100%'} h={'100%'}
> >
<Flex flexDirection="column" p="4" h={'100%'} overflow={'auto'}> <Flex flexDirection="column" p="4" h={'100%'} overflow={'auto'}>
<InputGroup alignItems="center" size={'sm'}> <SearchInput
<InputLeftElement> placeholder={t('user:search_user')}
<MyIcon name="common/searchLight" w="16px" color={'myGray.500'} /> fontSize="sm"
</InputLeftElement> bg={'myGray.50'}
<Input onChange={(e) => {
placeholder={t('user:search_user')} setSearchKey(e.target.value);
fontSize="sm" }}
bg={'myGray.50'} />
onChange={(e) => {
setSearchKey(e.target.value);
}}
/>
</InputGroup>
<Flex flexDirection="column" mt={3}> <Flex flexDirection="column" mt={3}>
{filtered.map((member) => { {filtered.map((member) => {
return ( return (

View File

@@ -12,7 +12,6 @@ export type AppUpdateParams = {
edges?: AppSchema['edges']; edges?: AppSchema['edges'];
chatConfig?: AppSchema['chatConfig']; chatConfig?: AppSchema['chatConfig'];
teamTags?: AppSchema['teamTags']; teamTags?: AppSchema['teamTags'];
defaultPermission?: AppSchema['defaultPermission'];
}; };
export type PostPublishAppProps = { export type PostPublishAppProps = {

View File

@@ -1,7 +1,8 @@
import { MongoMemoryServer } from 'mongodb-memory-server'; import { MongoMemoryReplSet } from 'mongodb-memory-server';
import mongoose from 'mongoose'; import mongoose from 'mongoose';
import { MockParseHeaderCert } from '@/test/utils'; import { parseHeaderCertMock } from '@/test/utils';
import { initMockData } from './db/init'; import { initMockData, root } from './db/init';
import { faker } from '@faker-js/faker/locale/zh_CN';
jest.mock('nanoid', () => { jest.mock('nanoid', () => {
return { return {
@@ -13,24 +14,40 @@ jest.mock('@fastgpt/global/common/string/tools', () => {
return { return {
hashStr(str: string) { hashStr(str: string) {
return str; return str;
},
getNanoid() {
return faker.string.alphanumeric(12);
} }
}; };
}); });
jest.mock('@fastgpt/service/common/system/log', jest.fn()); jest.mock('@fastgpt/service/common/system/log', () => ({
addLog: {
log: jest.fn(),
warn: jest.fn((...prop) => {
console.warn(prop);
}),
error: jest.fn((...prop) => {
console.error(prop);
}),
info: jest.fn(),
debug: jest.fn()
}
}));
jest.mock('@fastgpt/service/support/permission/controller', () => { jest.setMock(
return { '@fastgpt/service/support/permission/controller',
parseHeaderCert: MockParseHeaderCert, (() => {
getResourcePermission: jest.requireActual('@fastgpt/service/support/permission/controller') const origin = jest.requireActual<
.getResourcePermission, typeof import('@fastgpt/service/support/permission/controller')
getResourceAllClbs: jest.requireActual('@fastgpt/service/support/permission/controller') >('@fastgpt/service/support/permission/controller');
.getResourceAllClbs
};
});
const parse = jest.createMockFromModule('@fastgpt/service/support/permission/controller') as any; return {
parse.parseHeaderCert = MockParseHeaderCert; ...origin,
parseHeaderCert: parseHeaderCertMock
};
})()
);
jest.mock('@/service/middleware/entry', () => { jest.mock('@/service/middleware/entry', () => {
return { return {
@@ -59,11 +76,30 @@ jest.mock('@/service/middleware/entry', () => {
beforeAll(async () => { beforeAll(async () => {
// 新建一个内存数据库,然后让 mongoose 连接这个数据库 // 新建一个内存数据库,然后让 mongoose 连接这个数据库
if (!global.mongod || !global.mongodb) { if (!global.mongod || !global.mongodb) {
const mongod = await MongoMemoryServer.create(); const replSet = new MongoMemoryReplSet({
global.mongod = mongod; instanceOpts: [
{
storageEngine: 'wiredTiger'
},
{
storageEngine: 'wiredTiger'
}
]
});
replSet.start();
await replSet.waitUntilRunning();
const uri = replSet.getUri();
// const mongod = await MongoMemoryServer.create({
// instance: {
// replSet: 'testset'
// }
// });
// global.mongod = mongod;
global.replSet = replSet;
global.mongodb = mongoose; global.mongodb = mongoose;
await global.mongodb.connect(mongod.getUri(), { await global.mongodb.connect(uri, {
dbName: 'fastgpt_test',
bufferCommands: true, bufferCommands: true,
maxConnecting: 50, maxConnecting: 50,
maxPoolSize: 50, maxPoolSize: 50,
@@ -77,6 +113,7 @@ beforeAll(async () => {
}); });
await initMockData(); await initMockData();
console.log(root);
} }
}); });
@@ -84,6 +121,9 @@ afterAll(async () => {
if (global.mongodb) { if (global.mongodb) {
await global.mongodb.disconnect(); await global.mongodb.disconnect();
} }
if (global.replSet) {
await global.replSet.stop();
}
if (global.mongod) { if (global.mongod) {
await global.mongod.stop(); await global.mongod.stop();
} }

View File

@@ -1,5 +1,6 @@
import { TeamMemberRoleEnum } from '@fastgpt/global/support/user/team/constant'; import { DefaultGroupName } from '@fastgpt/global/support/user/team/group/constant';
import { MongoApp } from '@fastgpt/service/core/app/schema'; import { MongoApp } from '@fastgpt/service/core/app/schema';
import { MongoMemberGroupModel } from '@fastgpt/service/support/permission/memberGroup/memberGroupSchema';
import { MongoUser } from '@fastgpt/service/support/user/schema'; import { MongoUser } from '@fastgpt/service/support/user/schema';
import { MongoTeamMember } from '@fastgpt/service/support/user/team/teamMemberSchema'; import { MongoTeamMember } from '@fastgpt/service/support/user/team/teamMemberSchema';
import { MongoTeam } from '@fastgpt/service/support/user/team/teamSchema'; import { MongoTeam } from '@fastgpt/service/support/user/team/teamSchema';
@@ -13,37 +14,43 @@ export const root = {
}; };
export const initMockData = async () => { export const initMockData = async () => {
const initRootUser = async () => { const [rootUser] = await MongoUser.create([
// init root user {
const rootUser = await MongoUser.create({
username: 'root', username: 'root',
password: '123456' password: '123456'
}); }
]);
const rootTeam = await MongoTeam.create({ root.uid = String(rootUser._id);
name: 'root-default-team', const [rootTeam] = await MongoTeam.create([
ownerId: rootUser._id {
}); name: 'root Team'
}
const rootTeamMember = await MongoTeamMember.create({ ]);
root.teamId = String(rootTeam._id);
const [rootTmb] = await MongoTeamMember.create([
{
teamId: rootTeam._id, teamId: rootTeam._id,
name: 'owner',
role: 'owner',
userId: rootUser._id, userId: rootUser._id,
name: 'root-default-team-member', status: 'active'
status: 'active', }
role: TeamMemberRoleEnum.owner ]);
}); root.tmbId = String(rootTmb._id);
const rootApp = await MongoApp.create({ await MongoMemberGroupModel.create([
name: 'root-default-app', {
name: DefaultGroupName,
teamId: rootTeam._id
}
]);
const [rootApp] = await MongoApp.create([
{
name: 'root Test App',
teamId: rootTeam._id, teamId: rootTeam._id,
tmbId: rootTeam._id, tmbId: rootTmb._id
type: 'advanced' }
}); ]);
root.uid = rootUser._id; root.appId = String(rootApp._id);
root.tmbId = rootTeamMember._id;
root.teamId = rootTeam._id;
root.appId = rootApp._id;
};
await initRootUser();
}; };

View File

@@ -1,4 +1,11 @@
import { MongoMemoryServer } from 'mongodb-memory-server'; import type { MongoMemoryReplSet, MongoMemoryServer } from 'mongodb-memory-server';
declare global { declare global {
var mongod: MongoMemoryServer | undefined; var mongod: MongoMemoryServer | undefined;
var replSet: MongoMemoryReplSet | undefined;
} }
export type RequestResponse<T = any> = {
code: number;
error?: string;
data?: T;
};

View File

@@ -2,6 +2,7 @@ import { AppTypeEnum } from '@fastgpt/global/core/app/constants';
import { MongoApp } from '@fastgpt/service/core/app/schema'; import { MongoApp } from '@fastgpt/service/core/app/schema';
import { authUserPer } from '@fastgpt/service/support/permission/user/auth'; import { authUserPer } from '@fastgpt/service/support/permission/user/auth';
import { import {
OwnerPermissionVal,
PerResourceTypeEnum, PerResourceTypeEnum,
WritePermissionVal WritePermissionVal
} from '@fastgpt/global/support/permission/constant'; } from '@fastgpt/global/support/permission/constant';
@@ -11,11 +12,12 @@ import { NextAPI } from '@/service/middleware/entry';
import { ParentIdType } from '@fastgpt/global/common/parentFolder/type'; import { ParentIdType } from '@fastgpt/global/common/parentFolder/type';
import { parseParentIdInMongo } from '@fastgpt/global/common/parentFolder/utils'; import { parseParentIdInMongo } from '@fastgpt/global/common/parentFolder/utils';
import { authApp } from '@fastgpt/service/support/permission/app/auth'; import { authApp } from '@fastgpt/service/support/permission/app/auth';
import { AppDefaultPermissionVal } from '@fastgpt/global/support/permission/app/constant';
import { mongoSessionRun } from '@fastgpt/service/common/mongo/sessionRun'; import { mongoSessionRun } from '@fastgpt/service/common/mongo/sessionRun';
import { CommonErrEnum } from '@fastgpt/global/common/error/code/common'; import { CommonErrEnum } from '@fastgpt/global/common/error/code/common';
import { syncCollaborators } from '@fastgpt/service/support/permission/inheritPermission'; import { syncCollaborators } from '@fastgpt/service/support/permission/inheritPermission';
import { getResourceAllClbs } from '@fastgpt/service/support/permission/controller'; import { getResourceClbsAndGroups } from '@fastgpt/service/support/permission/controller';
import { TeamWritePermissionVal } from '@fastgpt/global/support/permission/user/constant';
import { MongoResourcePermission } from '@fastgpt/service/support/permission/schema';
export type CreateAppFolderBody = { export type CreateAppFolderBody = {
parentId?: ParentIdType; parentId?: ParentIdType;
@@ -31,20 +33,21 @@ async function handler(req: ApiRequestProps<CreateAppFolderBody>) {
} }
// 凭证校验 // 凭证校验
const { teamId, tmbId } = await authUserPer({ req, authToken: true, per: WritePermissionVal }); const { teamId, tmbId } = await authUserPer({
const parentApp = await (async () => { req,
if (parentId) { authToken: true,
// if it is not a root folder per: TeamWritePermissionVal
return ( });
await authApp({
req, if (parentId) {
appId: parentId, // if it is not a root folder
per: WritePermissionVal, await authApp({
authToken: true req,
}) appId: parentId,
).app; // check the parent folder permission per: WritePermissionVal,
} authToken: true
})(); });
}
// Create app // Create app
await mongoSessionRun(async (session) => { await mongoSessionRun(async (session) => {
@@ -55,13 +58,11 @@ async function handler(req: ApiRequestProps<CreateAppFolderBody>) {
intro, intro,
teamId, teamId,
tmbId, tmbId,
type: AppTypeEnum.folder, type: AppTypeEnum.folder
// inheritPermission: !!parentApp ? true : false,
defaultPermission: !!parentApp ? parentApp.defaultPermission : AppDefaultPermissionVal
}); });
if (parentId) { if (parentId) {
const parentClbs = await getResourceAllClbs({ const parentClbsAndGroups = await getResourceClbsAndGroups({
teamId, teamId,
resourceId: parentId, resourceId: parentId,
resourceType: PerResourceTypeEnum.app, resourceType: PerResourceTypeEnum.app,
@@ -72,9 +73,25 @@ async function handler(req: ApiRequestProps<CreateAppFolderBody>) {
resourceType: PerResourceTypeEnum.app, resourceType: PerResourceTypeEnum.app,
teamId, teamId,
resourceId: app._id, resourceId: app._id,
collaborators: parentClbs, collaborators: parentClbsAndGroups,
session session
}); });
} else {
// Create default permission
await MongoResourcePermission.create(
[
{
resourceType: PerResourceTypeEnum.app,
teamId,
resourceId: app._id,
tmbId,
permission: OwnerPermissionVal
}
],
{
session
}
);
} }
}); });
} }

View File

@@ -15,6 +15,8 @@ import { AppDefaultPermissionVal } from '@fastgpt/global/support/permission/app/
import { authApp } from '@fastgpt/service/support/permission/app/auth'; import { authApp } from '@fastgpt/service/support/permission/app/auth';
import { authUserPer } from '@fastgpt/service/support/permission/user/auth'; import { authUserPer } from '@fastgpt/service/support/permission/user/auth';
import { replaceRegChars } from '@fastgpt/global/common/string/tools'; import { replaceRegChars } from '@fastgpt/global/common/string/tools';
import { getGroupPer } from '@fastgpt/service/support/permission/controller';
import { getGroupsByTmbId } from '@fastgpt/service/support/permission/memberGroup/controllers';
export type ListAppBody = { export type ListAppBody = {
parentId?: ParentIdType; parentId?: ParentIdType;
@@ -31,7 +33,7 @@ async function handler(req: ApiRequestProps<ListAppBody>): Promise<AppListItemTy
app: ParentApp, app: ParentApp,
tmbId, tmbId,
teamId, teamId,
permission: tmbPer permission: myPer
} = await (async () => { } = await (async () => {
if (parentId) { if (parentId) {
return await authApp({ return await authApp({
@@ -87,10 +89,17 @@ async function handler(req: ApiRequestProps<ListAppBody>): Promise<AppListItemTy
})(); })();
/* temp: get all apps and per */ /* temp: get all apps and per */
const [myApps, rpList] = await Promise.all([ const myGroupIds = (
await getGroupsByTmbId({
tmbId,
teamId
})
).map((item) => String(item._id));
const [myApps, perList] = await Promise.all([
MongoApp.find( MongoApp.find(
findAppsQuery, findAppsQuery,
'_id parentId avatar type name intro tmbId updateTime pluginData defaultPermission inheritPermission' '_id parentId avatar type name intro tmbId updateTime pluginData inheritPermission'
) )
.sort({ .sort({
updateTime: -1 updateTime: -1
@@ -98,41 +107,67 @@ async function handler(req: ApiRequestProps<ListAppBody>): Promise<AppListItemTy
.limit(searchKey ? 20 : 1000) .limit(searchKey ? 20 : 1000)
.lean(), .lean(),
MongoResourcePermission.find({ MongoResourcePermission.find({
resourceType: PerResourceTypeEnum.app, $and: [
teamId, {
tmbId resourceType: PerResourceTypeEnum.app,
teamId,
resourceId: {
$exists: true
}
},
{ $or: [{ tmbId }, { groupId: { $in: myGroupIds } }] }
]
}).lean() }).lean()
]); ]);
const filterApps = myApps const filterApps = myApps
.map((app) => { .map((app) => {
const Per = (() => { const { Per, privateApp } = (() => {
// Inherit app // Inherit app
if (app.inheritPermission && ParentApp && !AppFolderTypeList.includes(app.type)) { if (app.inheritPermission && ParentApp && !AppFolderTypeList.includes(app.type)) {
// get its parent's permission as its permission const tmbPer = perList.find(
app.defaultPermission = ParentApp.defaultPermission; (item) => String(item.resourceId) === String(ParentApp._id) && !!item.tmbId
const perVal = rpList.find(
(item) => String(item.resourceId) === String(ParentApp._id)
)?.permission; )?.permission;
const groupPer = getGroupPer(
perList
.filter(
(item) =>
String(item.resourceId) === String(ParentApp._id) &&
myGroupIds.includes(String(item.groupId))
)
.map((item) => item.permission)
);
return new AppPermission({ return {
per: perVal ?? app.defaultPermission, Per: new AppPermission({
isOwner: String(app.tmbId) === String(tmbId) || tmbPer.isOwner per: tmbPer ?? groupPer ?? AppDefaultPermissionVal,
}); isOwner: String(app.tmbId) === String(tmbId) || myPer.isOwner
}),
privateApp: !tmbPer && !groupPer
};
} else { } else {
const perVal = rpList.find( const tmbPer = perList.find(
(item) => String(item.resourceId) === String(app._id) (item) => String(item.resourceId) === String(app._id) && !!item.tmbId
)?.permission; )?.permission;
return new AppPermission({ const group = perList.filter(
per: perVal ?? app.defaultPermission, (item) =>
isOwner: String(app.tmbId) === String(tmbId) || tmbPer.isOwner String(item.resourceId) === String(app._id) &&
}); myGroupIds.includes(String(item.groupId))
);
const groupPer = getGroupPer(group.map((item) => item.permission));
return {
Per: new AppPermission({
per: tmbPer ?? groupPer ?? AppDefaultPermissionVal,
isOwner: String(app.tmbId) === String(tmbId) || myPer.isOwner
}),
privateApp: !tmbPer && !groupPer
};
} }
})(); })();
return { return {
...app, ...app,
permission: Per permission: Per,
privateApp: privateApp
}; };
}) })
.filter((app) => app.permission.hasReadPer); .filter((app) => app.permission.hasReadPer);
@@ -148,9 +183,9 @@ async function handler(req: ApiRequestProps<ListAppBody>): Promise<AppListItemTy
intro: app.intro, intro: app.intro,
updateTime: app.updateTime, updateTime: app.updateTime,
permission: app.permission, permission: app.permission,
defaultPermission: app.defaultPermission || AppDefaultPermissionVal,
pluginData: app.pluginData, pluginData: app.pluginData,
inheritPermission: app.inheritPermission ?? true inheritPermission: app.inheritPermission ?? true,
private: app.privateApp
})); }));
} }

View File

@@ -6,6 +6,7 @@ import { NextAPI } from '@/service/middleware/entry';
import { import {
ManagePermissionVal, ManagePermissionVal,
PerResourceTypeEnum, PerResourceTypeEnum,
ReadPermissionVal,
WritePermissionVal WritePermissionVal
} from '@fastgpt/global/support/permission/constant'; } from '@fastgpt/global/support/permission/constant';
import { parseParentIdInMongo } from '@fastgpt/global/common/parentFolder/utils'; import { parseParentIdInMongo } from '@fastgpt/global/common/parentFolder/utils';
@@ -18,62 +19,78 @@ import {
import { AppFolderTypeList } from '@fastgpt/global/core/app/constants'; import { AppFolderTypeList } from '@fastgpt/global/core/app/constants';
import { ClientSession } from 'mongoose'; import { ClientSession } from 'mongoose';
import { mongoSessionRun } from '@fastgpt/service/common/mongo/sessionRun'; import { mongoSessionRun } from '@fastgpt/service/common/mongo/sessionRun';
import { PermissionValueType } from '@fastgpt/global/support/permission/type'; import { getResourceClbsAndGroups } from '@fastgpt/service/support/permission/controller';
import { getResourceAllClbs } from '@fastgpt/service/support/permission/controller'; import { authUserPer } from '@fastgpt/service/support/permission/user/auth';
import { AppDefaultPermissionVal } from '@fastgpt/global/support/permission/app/constant'; import { TeamWritePermissionVal } from '@fastgpt/global/support/permission/user/constant';
import { AppErrEnum } from '@fastgpt/global/common/error/code/app';
/* export type AppUpdateQuery = {
修改默认权限 appId: string;
1. 继承态目录:关闭继承态,修改权限,同步子目录默认权限 };
2. 继承态资源:关闭继承态,修改权限, 复制父级协作者。
3. 非继承目录:修改权限,同步子目录默认权限
4. 非继承资源:修改权限
移动 export type AppUpdateBody = AppUpdateParams;
1. 继承态目录:改 parentId, 修改成父的默认权限,同步子目录默认权限和协作者
2. 继承态资源:改 parentId
3. 非继承:改 parentId
*/
async function handler(req: ApiRequestProps<AppUpdateParams, { appId: string }>) { // 更新应用接口
const { // 包括如下功能:
parentId, // 1. 更新应用的信息(包括名称,类型,头像,介绍等)
name, // 2. 更新应用的编排信息
avatar, // 3. 移动应用
type, // 操作权限:
intro, // 1. 更新信息和工作流编排需要有应用的写权限
nodes, // 2. 移动应用需要有
edges, // (1) 父目录的管理权限
chatConfig, // (2) 目标目录的管理权限
teamTags, // (3) 如果从根目录移动或移动到根目录,需要有团队的应用创建权限
defaultPermission async function handler(req: ApiRequestProps<AppUpdateBody, AppUpdateQuery>) {
} = req.body as AppUpdateParams; const { parentId, name, avatar, type, intro, nodes, edges, chatConfig, teamTags } = req.body;
const { appId } = req.query as { appId: string }; const { appId } = req.query;
if (!appId) { if (!appId) {
Promise.reject(CommonErrEnum.missingParams); Promise.reject(CommonErrEnum.missingParams);
} }
const isMove = parentId !== undefined;
const { app } = await (async () => { // this step is to get the app and its permission, and we will check the permission manually for
if (defaultPermission !== undefined) { // different cases
// if defaultPermission or inheritPermission is set, then need manage permission const { app, permission } = await authApp({
return authApp({ req, authToken: true, appId, per: ManagePermissionVal }); req,
} else { authToken: true,
return authApp({ req, authToken: true, appId, per: WritePermissionVal }); appId,
per: ReadPermissionVal
});
if (!app) {
Promise.reject(AppErrEnum.unExist);
}
if (isMove) {
if (parentId) {
// move to a folder, check the target folder's permission
await authApp({ req, authToken: true, appId: parentId, per: ManagePermissionVal });
} }
})(); if (app.parentId) {
// move from a folder, check the (old) folder's permission
await authApp({ req, authToken: true, appId: app.parentId, per: ManagePermissionVal });
}
if (parentId === null || !app.parentId) {
// move to root or move from root
await authUserPer({
req,
authToken: true,
per: TeamWritePermissionVal
});
}
} else {
// is not move, write permission of the app.
if (!permission.hasWritePer) {
return Promise.reject(AppErrEnum.unAuthApp);
}
}
// format nodes data const onUpdate = async (session?: ClientSession) => {
// 1. dataset search limit, less than model quoteMaxToken // format nodes data
const isDefaultPermissionChanged = // 1. dataset search limit, less than model quoteMaxToken
defaultPermission !== undefined && defaultPermission !== app.defaultPermission;
const isFolder = AppFolderTypeList.includes(app.type);
const onUpdate = async (
session?: ClientSession,
updatedDefaultPermission?: PermissionValueType
) => {
const { nodes: formatNodes } = beforeUpdateAppFormat({ nodes }); const { nodes: formatNodes } = beforeUpdateAppFormat({ nodes });
return MongoApp.findByIdAndUpdate( return MongoApp.findByIdAndUpdate(
@@ -84,12 +101,6 @@ async function handler(req: ApiRequestProps<AppUpdateParams, { appId: string }>)
...(type && { type }), ...(type && { type }),
...(avatar && { avatar }), ...(avatar && { avatar }),
...(intro !== undefined && { intro }), ...(intro !== undefined && { intro }),
// update default permission(Maybe move update)
...(updatedDefaultPermission !== undefined && {
defaultPermission: updatedDefaultPermission
}),
// Not root, update default permission
...(app.parentId && isDefaultPermissionChanged && { inheritPermission: false }),
...(teamTags && { teamTags }), ...(teamTags && { teamTags }),
...(formatNodes && { ...(formatNodes && {
modules: formatNodes modules: formatNodes
@@ -97,34 +108,19 @@ async function handler(req: ApiRequestProps<AppUpdateParams, { appId: string }>)
...(edges && { ...(edges && {
edges edges
}), }),
...(chatConfig && { chatConfig }) ...(chatConfig && { chatConfig }),
...(isMove && { inheritPermission: true })
}, },
{ session } { session }
); );
}; };
// Move // Move
if (parentId !== undefined) { if (isMove) {
await mongoSessionRun(async (session) => { await mongoSessionRun(async (session) => {
// Auth
const parentDefaultPermission = await (async () => {
if (parentId) {
const { app: parentApp } = await authApp({
req,
authToken: true,
appId: parentId,
per: WritePermissionVal
});
return parentApp.defaultPermission;
}
return AppDefaultPermissionVal;
})();
// Inherit folder: Sync children permission and it's clbs // Inherit folder: Sync children permission and it's clbs
if (isFolder && app.inheritPermission) { if (AppFolderTypeList.includes(app.type)) {
const parentClbs = await getResourceAllClbs({ const parentClbsAndGroups = await getResourceClbsAndGroups({
teamId: app.teamId, teamId: app.teamId,
resourceId: parentId, resourceId: parentId,
resourceType: PerResourceTypeEnum.app, resourceType: PerResourceTypeEnum.app,
@@ -134,7 +130,7 @@ async function handler(req: ApiRequestProps<AppUpdateParams, { appId: string }>)
await syncCollaborators({ await syncCollaborators({
resourceId: app._id, resourceId: app._id,
resourceType: PerResourceTypeEnum.app, resourceType: PerResourceTypeEnum.app,
collaborators: parentClbs, collaborators: parentClbsAndGroups,
session, session,
teamId: app.teamId teamId: app.teamId
}); });
@@ -144,53 +140,12 @@ async function handler(req: ApiRequestProps<AppUpdateParams, { appId: string }>)
resourceType: PerResourceTypeEnum.app, resourceType: PerResourceTypeEnum.app,
resourceModel: MongoApp, resourceModel: MongoApp,
folderTypeList: AppFolderTypeList, folderTypeList: AppFolderTypeList,
defaultPermission: parentDefaultPermission, collaborators: parentClbsAndGroups,
collaborators: parentClbs,
session session
}); });
return onUpdate(session, parentDefaultPermission);
} }
return onUpdate(session); return onUpdate(session);
}); });
} else if (isDefaultPermissionChanged) {
// Update default permission
await mongoSessionRun(async (session) => {
if (isFolder) {
// Sync children default permission
await syncChildrenPermission({
resource: {
_id: app._id,
type: app.type,
teamId: app.teamId,
parentId: app.parentId
},
folderTypeList: AppFolderTypeList,
resourceModel: MongoApp,
resourceType: PerResourceTypeEnum.app,
session,
defaultPermission
});
} else if (app.inheritPermission && app.parentId) {
// Inherit app
const parentClbs = await getResourceAllClbs({
teamId: app.teamId,
resourceId: app.parentId,
resourceType: PerResourceTypeEnum.app,
session
});
await syncCollaborators({
resourceId: app._id,
resourceType: PerResourceTypeEnum.app,
collaborators: parentClbs,
session,
teamId: app.teamId
});
}
return onUpdate(session, defaultPermission);
});
} else { } else {
return onUpdate(); return onUpdate();
} }

View File

@@ -2,7 +2,6 @@ import type { NextApiRequest } from 'next';
import { MongoDataset } from '@fastgpt/service/core/dataset/schema'; import { MongoDataset } from '@fastgpt/service/core/dataset/schema';
import { getVectorModel } from '@fastgpt/service/core/ai/model'; import { getVectorModel } from '@fastgpt/service/core/ai/model';
import type { DatasetSimpleItemType } from '@fastgpt/global/core/dataset/type.d'; import type { DatasetSimpleItemType } from '@fastgpt/global/core/dataset/type.d';
import { DatasetTypeEnum } from '@fastgpt/global/core/dataset/constants';
import { NextAPI } from '@/service/middleware/entry'; import { NextAPI } from '@/service/middleware/entry';
import { import {
PerResourceTypeEnum, PerResourceTypeEnum,
@@ -11,6 +10,9 @@ import {
import { MongoResourcePermission } from '@fastgpt/service/support/permission/schema'; import { MongoResourcePermission } from '@fastgpt/service/support/permission/schema';
import { DatasetPermission } from '@fastgpt/global/support/permission/dataset/controller'; import { DatasetPermission } from '@fastgpt/global/support/permission/dataset/controller';
import { authUserPer } from '@fastgpt/service/support/permission/user/auth'; import { authUserPer } from '@fastgpt/service/support/permission/user/auth';
import { DatasetDefaultPermissionVal } from '@fastgpt/global/support/permission/dataset/constant';
import { getGroupsByTmbId } from '@fastgpt/service/support/permission/memberGroup/controllers';
import { getGroupPer } from '@fastgpt/service/support/permission/controller';
/* get all dataset by teamId or tmbId */ /* get all dataset by teamId or tmbId */
async function handler(req: NextApiRequest): Promise<DatasetSimpleItemType[]> { async function handler(req: NextApiRequest): Promise<DatasetSimpleItemType[]> {
@@ -25,7 +27,14 @@ async function handler(req: NextApiRequest): Promise<DatasetSimpleItemType[]> {
per: ReadPermissionVal per: ReadPermissionVal
}); });
const [myDatasets, rpList] = await Promise.all([ const myGroupIds = (
await getGroupsByTmbId({
tmbId,
teamId
})
).map((item) => String(item._id));
const [myDatasets, perList] = await Promise.all([
MongoDataset.find({ MongoDataset.find({
teamId teamId
}) })
@@ -34,39 +43,59 @@ async function handler(req: NextApiRequest): Promise<DatasetSimpleItemType[]> {
}) })
.lean(), .lean(),
MongoResourcePermission.find({ MongoResourcePermission.find({
resourceType: PerResourceTypeEnum.dataset, $and: [
teamId, {
tmbId resourceType: PerResourceTypeEnum.dataset,
teamId,
resourceId: {
$exists: true
}
},
{ $or: [{ tmbId }, { groupId: { $in: myGroupIds } }] }
]
}).lean() }).lean()
]); ]);
const filterDatasets = myDatasets const filterDatasets = myDatasets
.map((dataset) => { .map((dataset) => {
const perVal = (() => { const perVal = (() => {
const perVal = rpList.find( const parentDataset = myDatasets.find(
(item) => String(item.resourceId) === String(dataset._id) (item) => String(item._id) === String(dataset.parentId)
)?.permission; );
if (perVal) {
return perVal;
}
if (dataset.inheritPermission && dataset.parentId) { if (dataset.inheritPermission && dataset.parentId && parentDataset) {
const parentDataset = myDatasets.find( const tmbPer = perList.find(
(item) => String(item._id) === String(dataset.parentId) (item) => String(item.resourceId) === String(parentDataset._id) && !!item.tmbId
)?.permission;
const groupPer = getGroupPer(
perList
.filter(
(item) =>
String(item.resourceId) === String(parentDataset._id) &&
myGroupIds.includes(String(item.groupId))
)
.map((item) => item.permission)
); );
if (parentDataset) { return tmbPer ?? groupPer ?? DatasetDefaultPermissionVal;
const parentPerVal = } else {
rpList.find((item) => String(item.resourceId) === String(parentDataset._id)) const tmbPer = perList.find(
?.permission ?? parentDataset.defaultPermission; (item) => String(item.resourceId) === String(dataset._id) && !!item.tmbId
if (parentPerVal) { )?.permission;
return parentPerVal; const groupPer = getGroupPer(
} perList
} .filter(
(item) =>
String(item.resourceId) === String(dataset._id) &&
myGroupIds.includes(String(item.groupId))
)
.map((item) => item.permission)
);
return tmbPer ?? groupPer ?? DatasetDefaultPermissionVal;
} }
})(); })();
const Per = new DatasetPermission({ const Per = new DatasetPermission({
per: perVal ?? dataset.defaultPermission, per: perVal ?? DatasetDefaultPermissionVal,
isOwner: String(dataset.tmbId) === tmbId || tmbPer.isOwner isOwner: String(dataset.tmbId) === tmbId || tmbPer.isOwner
}); });

View File

@@ -4,6 +4,7 @@ import { MongoDataset } from '@fastgpt/service/core/dataset/schema';
import { CommonErrEnum } from '@fastgpt/global/common/error/code/common'; import { CommonErrEnum } from '@fastgpt/global/common/error/code/common';
import { authUserPer } from '@fastgpt/service/support/permission/user/auth'; import { authUserPer } from '@fastgpt/service/support/permission/user/auth';
import { import {
OwnerPermissionVal,
PerResourceTypeEnum, PerResourceTypeEnum,
WritePermissionVal WritePermissionVal
} from '@fastgpt/global/support/permission/constant'; } from '@fastgpt/global/support/permission/constant';
@@ -12,9 +13,9 @@ import { mongoSessionRun } from '@fastgpt/service/common/mongo/sessionRun';
import { parseParentIdInMongo } from '@fastgpt/global/common/parentFolder/utils'; import { parseParentIdInMongo } from '@fastgpt/global/common/parentFolder/utils';
import { FolderImgUrl } from '@fastgpt/global/common/file/image/constants'; import { FolderImgUrl } from '@fastgpt/global/common/file/image/constants';
import { DatasetTypeEnum } from '@fastgpt/global/core/dataset/constants'; import { DatasetTypeEnum } from '@fastgpt/global/core/dataset/constants';
import { DatasetDefaultPermissionVal } from '@fastgpt/global/support/permission/dataset/constant'; import { getResourceClbsAndGroups } from '@fastgpt/service/support/permission/controller';
import { getResourceAllClbs } from '@fastgpt/service/support/permission/controller';
import { syncCollaborators } from '@fastgpt/service/support/permission/inheritPermission'; import { syncCollaborators } from '@fastgpt/service/support/permission/inheritPermission';
import { MongoResourcePermission } from '@fastgpt/service/support/permission/schema';
export type DatasetFolderCreateQuery = {}; export type DatasetFolderCreateQuery = {};
export type DatasetFolderCreateBody = { export type DatasetFolderCreateBody = {
parentId?: string; parentId?: string;
@@ -38,35 +39,28 @@ async function handler(
authToken: true authToken: true
}); });
const parentFolder = await (async () => { if (parentId) {
if (parentId) { await authDataset({
return ( datasetId: parentId,
await authDataset({ per: WritePermissionVal,
datasetId: parentId, req,
per: WritePermissionVal, authToken: true
req, });
authToken: true }
})
).dataset;
}
})();
await mongoSessionRun(async (session) => { await mongoSessionRun(async (session) => {
const app = await MongoDataset.create({ const dataset = await MongoDataset.create({
...parseParentIdInMongo(parentId), ...parseParentIdInMongo(parentId),
avatar: FolderImgUrl, avatar: FolderImgUrl,
name, name,
intro, intro,
teamId, teamId,
tmbId, tmbId,
type: DatasetTypeEnum.folder, type: DatasetTypeEnum.folder
defaultPermission: !!parentFolder
? parentFolder.defaultPermission
: DatasetDefaultPermissionVal
}); });
if (parentId) { if (parentId) {
const parentClbs = await getResourceAllClbs({ const parentClbsAndGroups = await getResourceClbsAndGroups({
teamId, teamId,
resourceId: parentId, resourceId: parentId,
resourceType: PerResourceTypeEnum.dataset, resourceType: PerResourceTypeEnum.dataset,
@@ -76,11 +70,26 @@ async function handler(
await syncCollaborators({ await syncCollaborators({
resourceType: PerResourceTypeEnum.dataset, resourceType: PerResourceTypeEnum.dataset,
teamId, teamId,
resourceId: app._id, resourceId: dataset._id,
collaborators: parentClbs, collaborators: parentClbsAndGroups,
session session
}); });
} }
if (!parentId) {
await MongoResourcePermission.create(
[
{
resourceType: PerResourceTypeEnum.dataset,
teamId,
resourceId: dataset._id,
tmbId,
permission: OwnerPermissionVal
}
],
{ session }
);
}
}); });
return {}; return {};

View File

@@ -16,6 +16,8 @@ import { parseParentIdInMongo } from '@fastgpt/global/common/parentFolder/utils'
import { ApiRequestProps } from '@fastgpt/service/type/next'; import { ApiRequestProps } from '@fastgpt/service/type/next';
import { authDataset } from '@fastgpt/service/support/permission/dataset/auth'; import { authDataset } from '@fastgpt/service/support/permission/dataset/auth';
import { replaceRegChars } from '@fastgpt/global/common/string/tools'; import { replaceRegChars } from '@fastgpt/global/common/string/tools';
import { getGroupsByTmbId } from '@fastgpt/service/support/permission/memberGroup/controllers';
import { getGroupPer } from '@fastgpt/service/support/permission/controller';
export type GetDatasetListBody = { export type GetDatasetListBody = {
parentId: ParentIdType; parentId: ParentIdType;
@@ -30,7 +32,7 @@ async function handler(req: ApiRequestProps<GetDatasetListBody>) {
dataset: parentDataset, dataset: parentDataset,
teamId, teamId,
tmbId, tmbId,
permission: tmbPer permission: myPer
} = await (async () => { } = await (async () => {
if (parentId) { if (parentId) {
return await authDataset({ return await authDataset({
@@ -76,44 +78,84 @@ async function handler(req: ApiRequestProps<GetDatasetListBody>) {
}; };
})(); })();
const [myDatasets, rpList] = await Promise.all([ const myGroupIds = (
await getGroupsByTmbId({
tmbId,
teamId
})
).map((item) => String(item._id));
const [myDatasets, perList] = await Promise.all([
MongoDataset.find(findDatasetQuery) MongoDataset.find(findDatasetQuery)
.sort({ .sort({
updateTime: -1 updateTime: -1
}) })
.lean(), .lean(),
MongoResourcePermission.find({ MongoResourcePermission.find({
resourceType: PerResourceTypeEnum.dataset, $and: [
teamId, {
tmbId resourceType: PerResourceTypeEnum.dataset,
teamId,
resourceId: {
$exists: true
}
},
{ $or: [{ tmbId }, { groupId: { $in: myGroupIds } }] }
]
}).lean() }).lean()
]); ]);
const filterDatasets = myDatasets const filterDatasets = myDatasets
.map((dataset) => { .map((dataset) => {
const Per = (() => { const { Per, privateDataset } = (() => {
// inherit
if (dataset.inheritPermission && parentDataset && dataset.type !== DatasetTypeEnum.folder) { if (dataset.inheritPermission && parentDataset && dataset.type !== DatasetTypeEnum.folder) {
dataset.defaultPermission = parentDataset.defaultPermission; const tmbPer = perList.find(
const perVal = rpList.find( (item) => String(item.resourceId) === String(parentDataset._id) && !!item.tmbId
(item) => String(item.resourceId) === String(parentDataset._id)
)?.permission; )?.permission;
return new DatasetPermission({ const groupPer = getGroupPer(
per: perVal ?? parentDataset.defaultPermission, perList
isOwner: String(parentDataset.tmbId) === tmbId || tmbPer.isOwner .filter(
}); (item) =>
String(item.resourceId) === String(parentDataset._id) &&
myGroupIds.includes(String(item.groupId))
)
.map((item) => item.permission)
);
return {
Per: new DatasetPermission({
per: tmbPer ?? groupPer ?? DatasetDefaultPermissionVal,
isOwner: String(parentDataset.tmbId) === tmbId || myPer.isOwner
}),
privateDataset: !tmbPer && !groupPer
};
} else { } else {
const perVal = rpList.find( const tmbPer = perList.find(
(item) => String(item.resourceId) === String(dataset._id) (item) =>
String(item.resourceId) === String(dataset._id) && !!item.tmbId && !!item.permission
)?.permission; )?.permission;
return new DatasetPermission({ const groupPer = getGroupPer(
per: perVal ?? dataset.defaultPermission, perList
isOwner: String(dataset.tmbId) === tmbId || tmbPer.isOwner .filter(
}); (item) =>
String(item.resourceId) === String(dataset._id) &&
myGroupIds.includes(String(item.groupId))
)
.map((item) => item.permission)
);
return {
Per: new DatasetPermission({
per: tmbPer ?? groupPer ?? DatasetDefaultPermissionVal,
isOwner: String(dataset.tmbId) === tmbId || myPer.isOwner
}),
privateDataset: !tmbPer && !groupPer
};
} }
})(); })();
return { return {
...dataset, ...dataset,
permission: Per permission: Per,
privateDataset
}; };
}) })
.filter((app) => app.permission.hasReadPer); .filter((app) => app.permission.hasReadPer);
@@ -127,10 +169,10 @@ async function handler(req: ApiRequestProps<GetDatasetListBody>) {
type: item.type, type: item.type,
permission: item.permission, permission: item.permission,
vectorModel: getVectorModel(item.vectorModel), vectorModel: getVectorModel(item.vectorModel),
defaultPermission: item.defaultPermission ?? DatasetDefaultPermissionVal,
inheritPermission: item.inheritPermission, inheritPermission: item.inheritPermission,
tmbId: item.tmbId, tmbId: item.tmbId,
updateTime: item.updateTime updateTime: item.updateTime,
private: item.privateDataset
})) }))
); );

View File

@@ -14,6 +14,7 @@ import {
import { NextAPI } from '@/service/middleware/entry'; import { NextAPI } from '@/service/middleware/entry';
import { ReadPermissionVal } from '@fastgpt/global/support/permission/constant'; import { ReadPermissionVal } from '@fastgpt/global/support/permission/constant';
import { CommonErrEnum } from '@fastgpt/global/common/error/code/common'; import { CommonErrEnum } from '@fastgpt/global/common/error/code/common';
import { useReqFrequencyLimit } from '@fastgpt/service/common/middle/reqFrequencyLimit';
async function handler(req: NextApiRequest) { async function handler(req: NextApiRequest) {
const { const {
@@ -98,4 +99,4 @@ async function handler(req: NextApiRequest) {
}; };
} }
export default NextAPI(handler); export default NextAPI(useReqFrequencyLimit(1, 2), handler);

View File

@@ -5,63 +5,86 @@ import { NextAPI } from '@/service/middleware/entry';
import { import {
ManagePermissionVal, ManagePermissionVal,
PerResourceTypeEnum, PerResourceTypeEnum,
WritePermissionVal ReadPermissionVal
} from '@fastgpt/global/support/permission/constant'; } from '@fastgpt/global/support/permission/constant';
import { CommonErrEnum } from '@fastgpt/global/common/error/code/common'; import { CommonErrEnum } from '@fastgpt/global/common/error/code/common';
import type { ApiRequestProps, ApiResponseType } from '@fastgpt/service/type/next'; import type { ApiRequestProps, ApiResponseType } from '@fastgpt/service/type/next';
import { DatasetTypeEnum } from '@fastgpt/global/core/dataset/constants'; import { DatasetTypeEnum } from '@fastgpt/global/core/dataset/constants';
import { ClientSession } from 'mongoose'; import { ClientSession } from 'mongoose';
import { PermissionValueType } from '@fastgpt/global/support/permission/type';
import { parseParentIdInMongo } from '@fastgpt/global/common/parentFolder/utils'; import { parseParentIdInMongo } from '@fastgpt/global/common/parentFolder/utils';
import { mongoSessionRun } from '@fastgpt/service/common/mongo/sessionRun'; import { mongoSessionRun } from '@fastgpt/service/common/mongo/sessionRun';
import { DatasetDefaultPermissionVal } from '@fastgpt/global/support/permission/dataset/constant'; import { getResourceClbsAndGroups } from '@fastgpt/service/support/permission/controller';
import { DatasetSchemaType } from '@fastgpt/global/core/dataset/type';
import { getResourceAllClbs } from '@fastgpt/service/support/permission/controller';
import { import {
syncChildrenPermission, syncChildrenPermission,
syncCollaborators syncCollaborators
} from '@fastgpt/service/support/permission/inheritPermission'; } from '@fastgpt/service/support/permission/inheritPermission';
import { authUserPer } from '@fastgpt/service/support/permission/user/auth';
import { TeamWritePermissionVal } from '@fastgpt/global/support/permission/user/constant';
import { DatasetErrEnum } from '@fastgpt/global/common/error/code/dataset';
export type DatasetUpdateQuery = {}; export type DatasetUpdateQuery = {};
export type DatasetUpdateResponse = any; export type DatasetUpdateResponse = any;
// 更新知识库接口
// 包括如下功能:
// 1. 更新应用的信息(包括名称,类型,头像,介绍等)
// 2. 更新数据库的配置信息
// 3. 移动知识库
// 操作权限:
// 1. 更新信息和配置编排需要有知识库的写权限
// 2. 移动应用需要有
// (1) 父目录的管理权限
// (2) 目标目录的管理权限
// (3) 如果从根目录移动或移动到根目录,需要有团队的应用创建权限
async function handler( async function handler(
req: ApiRequestProps<DatasetUpdateBody, DatasetUpdateQuery>, req: ApiRequestProps<DatasetUpdateBody, DatasetUpdateQuery>,
_res: ApiResponseType<any> _res: ApiResponseType<any>
): Promise<DatasetUpdateResponse> { ): Promise<DatasetUpdateResponse> {
const { const { id, parentId, name, avatar, intro, agentModel, websiteConfig, externalReadUrl, status } =
id, req.body;
parentId,
name,
avatar,
intro,
agentModel,
websiteConfig,
externalReadUrl,
defaultPermission,
status
} = req.body;
if (!id) { if (!id) {
return Promise.reject(CommonErrEnum.missingParams); return Promise.reject(CommonErrEnum.missingParams);
} }
const { dataset } = (await (async () => { const isMove = parentId !== undefined;
if (defaultPermission !== undefined) {
return await authDataset({ req, authToken: true, datasetId: id, per: ManagePermissionVal }); const { dataset, permission } = await authDataset({
} else { req,
return await authDataset({ req, authToken: true, datasetId: id, per: WritePermissionVal }); authToken: true,
} datasetId: id,
})()) as { dataset: DatasetSchemaType }; per: ReadPermissionVal
});
if (isMove) {
if (parentId) {
// move to a folder, check the target folder's permission
await authDataset({ req, authToken: true, datasetId: parentId, per: ManagePermissionVal });
}
if (dataset.parentId) {
// move from a folder, check the (old) folder's permission
await authDataset({
req,
authToken: true,
datasetId: dataset.parentId,
per: ManagePermissionVal
});
}
if (parentId === null || !dataset.parentId) {
// move to root or move from root
await authUserPer({
req,
authToken: true,
per: TeamWritePermissionVal
});
}
} else {
// is not move
if (!permission.hasWritePer) return Promise.reject(DatasetErrEnum.unAuthDataset);
}
const isDefaultPermissionChanged =
defaultPermission !== undefined && dataset.defaultPermission !== defaultPermission;
const isFolder = dataset.type === DatasetTypeEnum.folder; const isFolder = dataset.type === DatasetTypeEnum.folder;
const onUpdate = async ( const onUpdate = async (session?: ClientSession) => {
session?: ClientSession,
updatedDefaultPermission?: PermissionValueType
) => {
await MongoDataset.findByIdAndUpdate( await MongoDataset.findByIdAndUpdate(
id, id,
{ {
@@ -73,35 +96,16 @@ async function handler(
...(status && { status }), ...(status && { status }),
...(intro !== undefined && { intro }), ...(intro !== undefined && { intro }),
...(externalReadUrl !== undefined && { externalReadUrl }), ...(externalReadUrl !== undefined && { externalReadUrl }),
// move ...(isMove && { inheritPermission: true })
...(updatedDefaultPermission !== undefined && {
defaultPermission: updatedDefaultPermission
}),
// update the defaultPermission
...(dataset.parentId && isDefaultPermissionChanged && { inheritPermission: false })
}, },
{ session } { session }
); );
}; };
// move if (isMove) {
if (parentId !== undefined) {
await mongoSessionRun(async (session) => { await mongoSessionRun(async (session) => {
const parentDefaultPermission = await (async () => {
if (parentId) {
const { dataset: parentDataset } = await authDataset({
req,
authToken: true,
datasetId: parentId,
per: WritePermissionVal
});
return parentDataset.defaultPermission;
}
return DatasetDefaultPermissionVal;
})();
if (isFolder && dataset.inheritPermission) { if (isFolder && dataset.inheritPermission) {
const parentClbs = await getResourceAllClbs({ const parentClbsAndGroups = await getResourceClbsAndGroups({
teamId: dataset.teamId, teamId: dataset.teamId,
resourceId: parentId, resourceId: parentId,
resourceType: PerResourceTypeEnum.dataset, resourceType: PerResourceTypeEnum.dataset,
@@ -112,7 +116,7 @@ async function handler(
teamId: dataset.teamId, teamId: dataset.teamId,
resourceId: id, resourceId: id,
resourceType: PerResourceTypeEnum.dataset, resourceType: PerResourceTypeEnum.dataset,
collaborators: parentClbs, collaborators: parentClbsAndGroups,
session session
}); });
@@ -121,48 +125,13 @@ async function handler(
resourceType: PerResourceTypeEnum.dataset, resourceType: PerResourceTypeEnum.dataset,
resourceModel: MongoDataset, resourceModel: MongoDataset,
folderTypeList: [DatasetTypeEnum.folder], folderTypeList: [DatasetTypeEnum.folder],
collaborators: parentClbs, collaborators: parentClbsAndGroups,
defaultPermission: parentDefaultPermission,
session session
}); });
return onUpdate(session, parentDefaultPermission); return onUpdate(session);
} }
return onUpdate(session); return onUpdate(session);
}); });
} else if (isDefaultPermissionChanged) {
await mongoSessionRun(async (session) => {
if (isFolder) {
await syncChildrenPermission({
defaultPermission,
resource: {
_id: dataset._id,
type: dataset.type,
teamId: dataset.teamId,
parentId: dataset.parentId
},
resourceType: PerResourceTypeEnum.dataset,
resourceModel: MongoDataset,
folderTypeList: [DatasetTypeEnum.folder],
session
});
} else if (dataset.inheritPermission && dataset.parentId) {
const parentClbs = await getResourceAllClbs({
teamId: dataset.teamId,
resourceId: parentId,
resourceType: PerResourceTypeEnum.dataset,
session
});
await syncCollaborators({
teamId: dataset.teamId,
resourceId: id,
resourceType: PerResourceTypeEnum.dataset,
collaborators: parentClbs,
session
});
}
return onUpdate(session, defaultPermission);
});
} else { } else {
return onUpdate(); return onUpdate();
} }

View File

@@ -1,12 +1,12 @@
import { getTestRequest } from '@/test/utils';
import '../../__mocks__/base'; import '../../__mocks__/base';
import { getTestRequest } from '@/test/utils';
import handler, { OutLinkUpdateBody, OutLinkUpdateQuery } from './update'; import handler, { OutLinkUpdateBody, OutLinkUpdateQuery } from './update';
import { root } from '../../__mocks__/db/init';
import { MongoOutLink } from '@fastgpt/service/support/outLink/schema'; import { MongoOutLink } from '@fastgpt/service/support/outLink/schema';
import { CommonErrEnum } from '@fastgpt/global/common/error/code/common'; import { CommonErrEnum } from '@fastgpt/global/common/error/code/common';
import { root } from '../../__mocks__/db/init';
test('Update Outlink', async () => { beforeAll(async () => {
const outlink = await MongoOutLink.create({ await MongoOutLink.create({
shareId: 'aaa', shareId: 'aaa',
appId: root.appId, appId: root.appId,
tmbId: root.tmbId, tmbId: root.tmbId,
@@ -14,8 +14,13 @@ test('Update Outlink', async () => {
type: 'share', type: 'share',
name: 'aaa' name: 'aaa'
}); });
});
await outlink.save(); test('Update Outlink', async () => {
const outlink = await MongoOutLink.findOne({ name: 'aaa' }).lean();
if (!outlink) {
throw new Error('Outlink not found');
}
const res = (await handler( const res = (await handler(
...getTestRequest<OutLinkUpdateQuery, OutLinkUpdateBody>({ ...getTestRequest<OutLinkUpdateQuery, OutLinkUpdateBody>({
@@ -27,6 +32,7 @@ test('Update Outlink', async () => {
}) })
)) as any; )) as any;
console.log(res);
expect(res.code).toBe(200); expect(res.code).toBe(200);
const link = await MongoOutLink.findById(outlink._id).lean(); const link = await MongoOutLink.findById(outlink._id).lean();

View File

@@ -28,16 +28,13 @@ import {
} from '@/web/core/app/api/collaborator'; } from '@/web/core/app/api/collaborator';
import { useContextSelector } from 'use-context-selector'; import { useContextSelector } from 'use-context-selector';
import { AppContext } from '@/pages/app/detail/components/context'; import { AppContext } from '@/pages/app/detail/components/context';
import { import { AppPermissionList } from '@fastgpt/global/support/permission/app/constant';
AppDefaultPermissionVal,
AppPermissionList
} from '@fastgpt/global/support/permission/app/constant';
import DefaultPermissionList from '@/components/support/permission/DefaultPerList';
import MyIcon from '@fastgpt/web/components/common/Icon'; import MyIcon from '@fastgpt/web/components/common/Icon';
import { resumeInheritPer } from '@/web/core/app/api'; import { resumeInheritPer } from '@/web/core/app/api';
import { useI18n } from '@/web/context/I18n'; import { useI18n } from '@/web/context/I18n';
import ResumeInherit from '@/components/support/permission/ResumeInheritText'; import ResumeInherit from '@/components/support/permission/ResumeInheritText';
import { PermissionValueType } from '@fastgpt/global/support/permission/type'; import { PermissionValueType } from '@fastgpt/global/support/permission/type';
import { RequireOnlyOne } from '@fastgpt/global/common/type/utils';
const InfoModal = ({ onClose }: { onClose: () => void }) => { const InfoModal = ({ onClose }: { onClose: () => void }) => {
const { t } = useTranslation(); const { t } = useTranslation();
@@ -67,8 +64,7 @@ const InfoModal = ({ onClose }: { onClose: () => void }) => {
await updateAppDetail({ await updateAppDetail({
name: data.name, name: data.name,
avatar: data.avatar, avatar: data.avatar,
intro: data.intro, intro: data.intro
defaultPermission: data.defaultPermission
}); });
}, },
{ {
@@ -129,24 +125,25 @@ const InfoModal = ({ onClose }: { onClose: () => void }) => {
const onUpdateCollaborators = ({ const onUpdateCollaborators = ({
members, members,
groups,
permission permission
}: { }: {
members: string[]; members?: string[];
groups?: string[];
permission: PermissionValueType; permission: PermissionValueType;
}) => { }) =>
return postUpdateAppCollaborators({ postUpdateAppCollaborators({
members, members,
groups,
permission, permission,
appId: appDetail._id appId: appDetail._id
}); });
};
const onDelCollaborator = (tmbId: string) => { const onDelCollaborator = async (props: RequireOnlyOne<{ tmbId: string; groupId: string }>) =>
return deleteAppCollaborators({ deleteAppCollaborators({
appId: appDetail._id, appId: appDetail._id,
tmbId ...props
}); });
};
const { runAsync: resumeInheritPermission } = useRequest2(() => resumeInheritPer(appDetail._id), { const { runAsync: resumeInheritPermission } = useRequest2(() => resumeInheritPer(appDetail._id), {
errorToast: t('common:resume_failed'), errorToast: t('common:resume_failed'),
@@ -204,33 +201,19 @@ const InfoModal = ({ onClose }: { onClose: () => void }) => {
<ResumeInherit onResume={resumeInheritPermission} /> <ResumeInherit onResume={resumeInheritPermission} />
</Box> </Box>
)} )}
<Box mt="4">
<Box fontSize={'sm'}>{t('common:permission.Default permission')}</Box>
<DefaultPermissionList
mt="2"
per={appDetail.defaultPermission}
defaultPer={AppDefaultPermissionVal}
isInheritPermission={appDetail.inheritPermission}
onChange={(v) => {
setValue('defaultPermission', v);
return handleSubmit((data) => saveSubmitSuccess(data), saveSubmitError)();
}}
hasParent={!!appDetail.parentId}
/>
</Box>
<Box mt={6}> <Box mt={6}>
<CollaboratorContextProvider <CollaboratorContextProvider
mode="all"
permission={appDetail.permission} permission={appDetail.permission}
onGetCollaboratorList={() => getCollaboratorList(appDetail._id)} onGetCollaboratorList={() => getCollaboratorList(appDetail._id)}
permissionList={AppPermissionList} permissionList={AppPermissionList}
onUpdateCollaborators={(props) => { onUpdateCollaborators={async (props) =>
if (props.members) { onUpdateCollaborators({
return onUpdateCollaborators({ permission: props.permission,
permission: props.permission, members: props.members,
members: props.members groups: props.groups
}); })
} }
}}
onDelOneCollaborator={onDelCollaborator} onDelOneCollaborator={onDelCollaborator}
refreshDeps={[appDetail.inheritPermission]} refreshDeps={[appDetail.inheritPermission]}
isInheritPermission={appDetail.inheritPermission} isInheritPermission={appDetail.inheritPermission}

View File

@@ -5,7 +5,6 @@ import {
Button, Button,
IconButton, IconButton,
HStack, HStack,
Modal,
ModalBody, ModalBody,
Checkbox, Checkbox,
ModalFooter ModalFooter
@@ -19,26 +18,23 @@ import TagsEditModal from '../TagsEditModal';
import { useSystemStore } from '@/web/common/system/useSystemStore'; import { useSystemStore } from '@/web/common/system/useSystemStore';
import { AppContext } from '@/pages/app/detail/components/context'; import { AppContext } from '@/pages/app/detail/components/context';
import { useContextSelector } from 'use-context-selector'; import { useContextSelector } from 'use-context-selector';
import PermissionIconText from '@/components/support/permission/IconText';
import MyTag from '@fastgpt/web/components/common/Tag/index';
import MyMenu from '@fastgpt/web/components/common/MyMenu'; import MyMenu from '@fastgpt/web/components/common/MyMenu';
import { useI18n } from '@/web/context/I18n'; import { useI18n } from '@/web/context/I18n';
import MyModal from '@fastgpt/web/components/common/MyModal'; import MyModal from '@fastgpt/web/components/common/MyModal';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest'; import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { postTransition2Workflow } from '@/web/core/app/api/app'; import { postTransition2Workflow } from '@/web/core/app/api/app';
import { AppTypeEnum } from '@fastgpt/global/core/app/constants'; import { AppTypeEnum } from '@fastgpt/global/core/app/constants';
import { useSystem } from '@fastgpt/web/hooks/useSystem';
const AppCard = () => { const AppCard = () => {
const router = useRouter(); const router = useRouter();
const { t } = useTranslation(); const { t } = useTranslation();
const { appT } = useI18n(); const { appT } = useI18n();
const { isPc } = useSystem();
const { appDetail, setAppDetail, onOpenInfoEdit, onDelApp } = useContextSelector( const { appDetail, setAppDetail, onOpenInfoEdit, onDelApp } = useContextSelector(
AppContext, AppContext,
(v) => v (v) => v
); );
const appId = appDetail._id; const appId = appDetail._id;
const { feConfigs } = useSystemStore(); const { feConfigs } = useSystemStore();
const [TeamTagsSet, setTeamTagsSet] = useState<AppSchema>(); const [TeamTagsSet, setTeamTagsSet] = useState<AppSchema>();
@@ -150,15 +146,15 @@ const AppCard = () => {
/> />
)} )}
<Box flex={1} /> <Box flex={1} />
{isPc && ( {/* {isPc && ( */}
<MyTag {/* <MyTag */}
type="borderFill" {/* type="borderFill" */}
colorSchema="gray" {/* colorSchema="gray" */}
onClick={() => (appDetail.permission.hasManagePer ? onOpenInfoEdit() : undefined)} {/* onClick={() => (appDetail.permission.hasManagePer ? onOpenInfoEdit() : undefined)} */}
> {/* > */}
<PermissionIconText defaultPermission={appDetail.defaultPermission} /> {/* <PermissionIconText defaultPermission={appDetail.defaultPermission} /> */}
</MyTag> {/* </MyTag> */}
)} {/* )} */}
</HStack> </HStack>
</Box> </Box>
{TeamTagsSet && <TagsEditModal onClose={() => setTeamTagsSet(undefined)} />} {TeamTagsSet && <TagsEditModal onClose={() => setTeamTagsSet(undefined)} />}

View File

@@ -34,7 +34,11 @@ const Header = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const { isPc } = useSystem(); const { isPc } = useSystem();
const router = useRouter(); const router = useRouter();
const { toast } = useToast(); const { toast: backSaveToast } = useToast({
containerStyle: {
mt: '60px'
}
});
const { appDetail, onSaveApp, currentTab } = useContextSelector(AppContext, (v) => v); const { appDetail, onSaveApp, currentTab } = useContextSelector(AppContext, (v) => v);
const isV2Workflow = appDetail?.version === 'v2'; const isV2Workflow = appDetail?.version === 'v2';
@@ -273,7 +277,7 @@ const Header = () => {
await onClickSave({}); await onClickSave({});
onCloseBackConfirm(); onCloseBackConfirm();
onBack(); onBack();
toast({ backSaveToast({
status: 'success', status: 'success',
title: t('app:saved_success'), title: t('app:saved_success'),
position: 'top-right' position: 'top-right'

View File

@@ -1,6 +1,6 @@
import { computedNodeInputReference } from '@/web/core/workflow/utils'; import { computedNodeInputReference } from '@/web/core/workflow/utils';
import { AppDetailType } from '@fastgpt/global/core/app/type'; import { AppDetailType } from '@fastgpt/global/core/app/type';
import { NodeInputKeyEnum } from '@fastgpt/global/core/workflow/constants'; import { NodeInputKeyEnum, NodeOutputKeyEnum } from '@fastgpt/global/core/workflow/constants';
import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant'; import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
import { StoreEdgeItemType } from '@fastgpt/global/core/workflow/type/edge'; import { StoreEdgeItemType } from '@fastgpt/global/core/workflow/type/edge';
import { FlowNodeItemType, StoreNodeItemType } from '@fastgpt/global/core/workflow/type/node.d'; import { FlowNodeItemType, StoreNodeItemType } from '@fastgpt/global/core/workflow/type/node.d';
@@ -125,7 +125,7 @@ export const getEditorVariables = ({
: sourceNodes : sourceNodes
.map((node) => { .map((node) => {
return node.outputs return node.outputs
.filter((output) => !!output.label) .filter((output) => !!output.label && output.id !== NodeOutputKeyEnum.addOutputParam)
.map((output) => { .map((output) => {
return { return {
label: t((output.label as any) || ''), label: t((output.label as any) || ''),

View File

@@ -1,4 +1,4 @@
import { Dispatch, ReactNode, SetStateAction, useCallback, useEffect, useState } from 'react'; import { Dispatch, ReactNode, SetStateAction, useCallback, useState } from 'react';
import { createContext } from 'use-context-selector'; import { createContext } from 'use-context-selector';
import { defaultApp } from '@/web/core/app/constants'; import { defaultApp } from '@/web/core/app/constants';
import { delAppById, getAppDetailById, putAppById } from '@/web/core/app/api'; import { delAppById, getAppDetailById, putAppById } from '@/web/core/app/api';

View File

@@ -17,10 +17,7 @@ import { useFolderDrag } from '@/components/common/folder/useFolderDrag';
import dynamic from 'next/dynamic'; import dynamic from 'next/dynamic';
import type { EditResourceInfoFormType } from '@/components/common/Modal/EditResourceModal'; import type { EditResourceInfoFormType } from '@/components/common/Modal/EditResourceModal';
import MyMenu from '@fastgpt/web/components/common/MyMenu'; import MyMenu from '@fastgpt/web/components/common/MyMenu';
import { import { AppPermissionList } from '@fastgpt/global/support/permission/app/constant';
AppDefaultPermissionVal,
AppPermissionList
} from '@fastgpt/global/support/permission/app/constant';
import { import {
deleteAppCollaborators, deleteAppCollaborators,
getCollaboratorList, getCollaboratorList,
@@ -38,6 +35,7 @@ import { formatTimeToChatTime } from '@fastgpt/global/common/string/time';
import { useSystem } from '@fastgpt/web/hooks/useSystem'; import { useSystem } from '@fastgpt/web/hooks/useSystem';
import { useChatStore } from '@/web/core/chat/context/storeChat'; import { useChatStore } from '@/web/core/chat/context/storeChat';
import { useUserStore } from '@/web/support/user/useUserStore'; import { useUserStore } from '@/web/support/user/useUserStore';
import { RequireOnlyOne } from '@fastgpt/global/common/type/utils';
const HttpEditModal = dynamic(() => import('./HttpPluginEditModal')); const HttpEditModal = dynamic(() => import('./HttpPluginEditModal'));
const ListItem = () => { const ListItem = () => {
@@ -49,11 +47,16 @@ const ListItem = () => {
const { loadAndGetTeamMembers } = useUserStore(); const { loadAndGetTeamMembers } = useUserStore();
const { lastChatAppId, setLastChatAppId } = useChatStore(); const { lastChatAppId, setLastChatAppId } = useChatStore();
const { openConfirm: openMoveConfirm, ConfirmModal: MoveConfirmModal } = useConfirm({
type: 'common',
title: t('common:move.confirm'),
content: t('app:move.hint')
});
const { myApps, loadMyApps, onUpdateApp, setMoveAppId, folderDetail } = useContextSelector( const { myApps, loadMyApps, onUpdateApp, setMoveAppId, folderDetail } = useContextSelector(
AppListContext, AppListContext,
(v) => v (v) => v
); );
const [loadingAppId, setLoadingAppId] = useState<string>();
const [editedApp, setEditedApp] = useState<EditResourceInfoFormType>(); const [editedApp, setEditedApp] = useState<EditResourceInfoFormType>();
const [editHttpPlugin, setEditHttpPlugin] = useState<EditHttpPluginProps>(); const [editHttpPlugin, setEditHttpPlugin] = useState<EditHttpPluginProps>();
@@ -64,17 +67,20 @@ const ListItem = () => {
[editPerAppIndex, myApps] [editPerAppIndex, myApps]
); );
const parentApp = useMemo(() => myApps.find((item) => item._id === parentId), [parentId, myApps]);
const { runAsync: onPutAppById } = useRequest2(putAppById, {
onSuccess() {
loadMyApps();
}
});
const { getBoxProps } = useFolderDrag({ const { getBoxProps } = useFolderDrag({
activeStyles: { activeStyles: {
borderColor: 'primary.600' borderColor: 'primary.600'
}, },
onDrop: async (dragId: string, targetId: string) => { onDrop: (dragId: string, targetId: string) => {
setLoadingAppId(dragId); openMoveConfirm(async () => onPutAppById(dragId, { parentId: targetId }))();
try {
await putAppById(dragId, { parentId: targetId });
loadMyApps();
} catch (error) {}
setLoadingAppId(undefined);
} }
}); });
@@ -152,7 +158,6 @@ const ListItem = () => {
} }
> >
<MyBox <MyBox
isLoading={loadingAppId === app._id}
lineHeight={1.5} lineHeight={1.5}
h="100%" h="100%"
pt={5} pt={5}
@@ -233,7 +238,7 @@ const ListItem = () => {
)} )}
<PermissionIconText <PermissionIconText
defaultPermission={app.defaultPermission} private={app.private}
color={'myGray.500'} color={'myGray.500'}
iconColor={'myGray.400'} iconColor={'myGray.400'}
w={'0.875rem'} w={'0.875rem'}
@@ -247,7 +252,9 @@ const ListItem = () => {
<Box color={'myGray.500'}>{formatTimeToChatTime(app.updateTime)}</Box> <Box color={'myGray.500'}>{formatTimeToChatTime(app.updateTime)}</Box>
</HStack> </HStack>
)} )}
{app.permission.hasWritePer && ( {(AppFolderTypeList.includes(app.type)
? app.permission.hasManagePer
: app.permission.hasWritePer) && (
<Box className="more" display={['', 'none']}> <Box className="more" display={['', 'none']}>
<MyMenu <MyMenu
Button={ Button={
@@ -315,7 +322,9 @@ const ListItem = () => {
} }
} }
}, },
...(folderDetail?.type === AppTypeEnum.httpPlugin ...(folderDetail?.type === AppTypeEnum.httpPlugin &&
!(parentApp ? parentApp.permission : app.permission)
.hasManagePer
? [] ? []
: [ : [
{ {
@@ -412,34 +421,29 @@ const ListItem = () => {
isInheritPermission={editPerApp.inheritPermission} isInheritPermission={editPerApp.inheritPermission}
avatar={editPerApp.avatar} avatar={editPerApp.avatar}
name={editPerApp.name} name={editPerApp.name}
defaultPer={{
value: editPerApp.defaultPermission,
defaultValue: AppDefaultPermissionVal,
onChange: (e) => {
return onUpdateApp(editPerApp._id, { defaultPermission: e });
}
}}
managePer={{ managePer={{
mode: 'all',
permission: editPerApp.permission, permission: editPerApp.permission,
onGetCollaboratorList: () => getCollaboratorList(editPerApp._id), onGetCollaboratorList: () => getCollaboratorList(editPerApp._id),
permissionList: AppPermissionList, permissionList: AppPermissionList,
onUpdateCollaborators: ({ onUpdateCollaborators: (props: {
members = [], // TODO: remove the default value after group is ready
permission
}: {
members?: string[]; members?: string[];
groups?: string[];
permission: number; permission: number;
}) => { }) =>
return postUpdateAppCollaborators({ postUpdateAppCollaborators({
members, ...props,
permission,
appId: editPerApp._id appId: editPerApp._id
}); }),
}, onDelOneCollaborator: async (
onDelOneCollaborator: (tmbId: string) => props: RequireOnlyOne<{
tmbId?: string;
groupId?: string;
}>
) =>
deleteAppCollaborators({ deleteAppCollaborators({
appId: editPerApp._id, ...props,
tmbId appId: editPerApp._id
}), }),
refreshDeps: [editPerApp.inheritPermission] refreshDeps: [editPerApp.inheritPermission]
}} }}
@@ -452,6 +456,7 @@ const ListItem = () => {
onClose={() => setEditHttpPlugin(undefined)} onClose={() => setEditHttpPlugin(undefined)}
/> />
)} )}
<MoveConfirmModal />
</> </>
); );
}; };

View File

@@ -12,9 +12,9 @@ import {
} from '@fastgpt/global/common/parentFolder/type'; } from '@fastgpt/global/common/parentFolder/type';
import { AppUpdateParams } from '@/global/core/app/api'; import { AppUpdateParams } from '@/global/core/app/api';
import dynamic from 'next/dynamic'; import dynamic from 'next/dynamic';
import { useI18n } from '@/web/context/I18n';
import { AppTypeEnum } from '@fastgpt/global/core/app/constants'; import { AppTypeEnum } from '@fastgpt/global/core/app/constants';
import { useSystemStore } from '@/web/common/system/useSystemStore'; import { useSystemStore } from '@/web/common/system/useSystemStore';
import { useTranslation } from 'next-i18next';
const MoveModal = dynamic(() => import('@/components/common/folder/MoveModal')); const MoveModal = dynamic(() => import('@/components/common/folder/MoveModal'));
type AppListContextType = { type AppListContextType = {
@@ -58,7 +58,7 @@ export const AppListContext = createContext<AppListContextType>({
}); });
const AppListContextProvider = ({ children }: { children: ReactNode }) => { const AppListContextProvider = ({ children }: { children: ReactNode }) => {
const { appT } = useI18n(); const { t } = useTranslation();
const router = useRouter(); const router = useRouter();
const { parentId = null, type = 'ALL' } = router.query as { const { parentId = null, type = 'ALL' } = router.query as {
parentId?: string | null; parentId?: string | null;
@@ -129,10 +129,12 @@ const AppListContextProvider = ({ children }: { children: ReactNode }) => {
parentId, parentId,
type: AppTypeEnum.folder type: AppTypeEnum.folder
}).then((res) => }).then((res) =>
res.map((item) => ({ res
id: item._id, .filter((item) => item.permission.hasWritePer)
name: item.name .map((item) => ({
})) id: item._id,
name: item.name
}))
); );
}, []); }, []);
@@ -162,9 +164,10 @@ const AppListContextProvider = ({ children }: { children: ReactNode }) => {
<MoveModal <MoveModal
moveResourceId={moveAppId} moveResourceId={moveAppId}
server={getAppFolderList} server={getAppFolderList}
title={appT('move_app')} title={t('app:move_app')}
onClose={() => setMoveAppId(undefined)} onClose={() => setMoveAppId(undefined)}
onConfirm={onMoveApp} onConfirm={onMoveApp}
moveHint={t('app:move.hint')}
/> />
)} )}
</AppListContext.Provider> </AppListContext.Provider>

View File

@@ -1,4 +1,4 @@
import React, { useEffect, useMemo, useState } from 'react'; import React, { useMemo, useState } from 'react';
import { import {
Box, Box,
Flex, Flex,
@@ -14,8 +14,6 @@ import { useUserStore } from '@/web/support/user/useUserStore';
import { useI18n } from '@/web/context/I18n'; import { useI18n } from '@/web/context/I18n';
import { useTranslation } from 'next-i18next'; import { useTranslation } from 'next-i18next';
import dynamic from 'next/dynamic'; import dynamic from 'next/dynamic';
import List from './components/List';
import MyMenu from '@fastgpt/web/components/common/MyMenu'; import MyMenu from '@fastgpt/web/components/common/MyMenu';
import { FolderIcon } from '@fastgpt/global/common/file/image/constants'; import { FolderIcon } from '@fastgpt/global/common/file/image/constants';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest'; import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
@@ -27,10 +25,7 @@ import FolderPath from '@/components/common/folder/Path';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import FolderSlideCard from '@/components/common/folder/SlideCard'; import FolderSlideCard from '@/components/common/folder/SlideCard';
import { delAppById, resumeInheritPer } from '@/web/core/app/api'; import { delAppById, resumeInheritPer } from '@/web/core/app/api';
import { import { AppPermissionList } from '@fastgpt/global/support/permission/app/constant';
AppDefaultPermissionVal,
AppPermissionList
} from '@fastgpt/global/support/permission/app/constant';
import { import {
deleteAppCollaborators, deleteAppCollaborators,
getCollaboratorList, getCollaboratorList,
@@ -49,6 +44,7 @@ const EditFolderModal = dynamic(
() => import('@fastgpt/web/components/common/MyModal/EditFolderModal') () => import('@fastgpt/web/components/common/MyModal/EditFolderModal')
); );
const HttpEditModal = dynamic(() => import('./components/HttpPluginEditModal')); const HttpEditModal = dynamic(() => import('./components/HttpPluginEditModal'));
const List = dynamic(() => import('./components/List'));
const MyApps = () => { const MyApps = () => {
const { t } = useTranslation(); const { t } = useTranslation();
@@ -273,36 +269,47 @@ const MyApps = () => {
onMove={() => setMoveAppId(folderDetail._id)} onMove={() => setMoveAppId(folderDetail._id)}
deleteTip={appT('confirm_delete_folder_tip')} deleteTip={appT('confirm_delete_folder_tip')}
onDelete={() => onDeleFolder(folderDetail._id)} onDelete={() => onDeleFolder(folderDetail._id)}
defaultPer={{
value: folderDetail.defaultPermission,
defaultValue: AppDefaultPermissionVal,
onChange: (e) => {
return onUpdateApp(folderDetail._id, { defaultPermission: e });
}
}}
managePer={{ managePer={{
mode: 'all',
permission: folderDetail.permission, permission: folderDetail.permission,
onGetCollaboratorList: () => getCollaboratorList(folderDetail._id), onGetCollaboratorList: () => getCollaboratorList(folderDetail._id),
permissionList: AppPermissionList, permissionList: AppPermissionList,
onUpdateCollaborators: ({ onUpdateCollaborators: ({
members = [], // TODO: remove the default value after group is ready members,
groups,
permission permission
}: { }: {
members?: string[]; members?: string[];
groups?: string[];
permission: number; permission: number;
}) => { }) => {
return postUpdateAppCollaborators({ return postUpdateAppCollaborators({
members, members,
groups,
permission, permission,
appId: folderDetail._id appId: folderDetail._id
}); });
}, },
refreshDeps: [folderDetail._id, folderDetail.inheritPermission], refreshDeps: [folderDetail._id, folderDetail.inheritPermission],
onDelOneCollaborator: (tmbId: string) => onDelOneCollaborator: async ({
deleteAppCollaborators({ tmbId,
appId: folderDetail._id, groupId
tmbId }: {
}) tmbId?: string;
groupId?: string;
}) => {
if (tmbId) {
return deleteAppCollaborators({
appId: folderDetail._id,
tmbId
});
} else if (groupId) {
return deleteAppCollaborators({
appId: folderDetail._id,
groupId
});
}
}
}} }}
/> />
</Box> </Box>

View File

@@ -1,7 +1,5 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { useRouter } from 'next/router';
import { Box, Flex, Input } from '@chakra-ui/react'; import { Box, Flex, Input } from '@chakra-ui/react';
import { delDatasetById } from '@/web/core/dataset/api';
import { useSelectFile } from '@/web/common/file/hooks/useSelectFile'; import { useSelectFile } from '@/web/common/file/hooks/useSelectFile';
import { useConfirm } from '@fastgpt/web/hooks/useConfirm'; import { useConfirm } from '@fastgpt/web/hooks/useConfirm';
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
@@ -10,7 +8,7 @@ import type { DatasetItemType } from '@fastgpt/global/core/dataset/type.d';
import Avatar from '@fastgpt/web/components/common/Avatar'; import Avatar from '@fastgpt/web/components/common/Avatar';
import { useTranslation } from 'next-i18next'; import { useTranslation } from 'next-i18next';
import { useSystemStore } from '@/web/common/system/useSystemStore'; import { useSystemStore } from '@/web/common/system/useSystemStore';
import { useRequest, useRequest2 } from '@fastgpt/web/hooks/useRequest'; import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { MongoImageTypeEnum } from '@fastgpt/global/common/file/image/constants'; import { MongoImageTypeEnum } from '@fastgpt/global/common/file/image/constants';
import AIModelSelector from '@/components/Select/AIModelSelector'; import AIModelSelector from '@/components/Select/AIModelSelector';
import { postRebuildEmbedding } from '@/web/core/dataset/api'; import { postRebuildEmbedding } from '@/web/core/dataset/api';
@@ -21,12 +19,8 @@ import MyDivider from '@fastgpt/web/components/common/MyDivider/index';
import { DatasetTypeEnum, DatasetTypeMap } from '@fastgpt/global/core/dataset/constants'; import { DatasetTypeEnum, DatasetTypeMap } from '@fastgpt/global/core/dataset/constants';
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip'; import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel'; import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel';
import DefaultPermissionList from '@/components/support/permission/DefaultPerList';
import MyIcon from '@fastgpt/web/components/common/Icon'; import MyIcon from '@fastgpt/web/components/common/Icon';
import { import { DatasetPermissionList } from '@fastgpt/global/support/permission/dataset/constant';
DatasetDefaultPermissionVal,
DatasetPermissionList
} from '@fastgpt/global/support/permission/dataset/constant';
import MemberManager from '../../component/MemberManager'; import MemberManager from '../../component/MemberManager';
import { import {
getCollaboratorList, getCollaboratorList,
@@ -39,7 +33,6 @@ import { EditResourceInfoFormType } from '@/components/common/Modal/EditResource
const EditResourceModal = dynamic(() => import('@/components/common/Modal/EditResourceModal')); const EditResourceModal = dynamic(() => import('@/components/common/Modal/EditResourceModal'));
const Info = ({ datasetId }: { datasetId: string }) => { const Info = ({ datasetId }: { datasetId: string }) => {
const router = useRouter();
const [openBaseConfig, setOpenBaseConfig] = useState(true); const [openBaseConfig, setOpenBaseConfig] = useState(true);
const [openPermissionConfig, setOpenPermissionConfig] = useState(true); const [openPermissionConfig, setOpenPermissionConfig] = useState(true);
const { t } = useTranslation(); const { t } = useTranslation();
@@ -56,10 +49,9 @@ const Info = ({ datasetId }: { datasetId: string }) => {
const vectorModel = watch('vectorModel'); const vectorModel = watch('vectorModel');
const agentModel = watch('agentModel'); const agentModel = watch('agentModel');
const defaultPermission = watch('defaultPermission');
const { datasetModelList, vectorModelList } = useSystemStore(); const { datasetModelList, vectorModelList } = useSystemStore();
const { openConfirm: onOpenConfirmDel, ConfirmModal: ConfirmDelModal } = useConfirm({ const { ConfirmModal: ConfirmDelModal } = useConfirm({
content: t('common:core.dataset.Delete Confirm'), content: t('common:core.dataset.Delete Confirm'),
type: 'delete' type: 'delete'
}); });
@@ -69,30 +61,17 @@ const Info = ({ datasetId }: { datasetId: string }) => {
type: 'delete' type: 'delete'
}); });
const { File, onOpen: onOpenSelectFile } = useSelectFile({ const { File } = useSelectFile({
fileType: '.jpg,.png', fileType: '.jpg,.png',
multiple: false multiple: false
}); });
/* 点击删除 */ const { runAsync: onSave } = useRequest2(
const { mutate: onclickDelete, isLoading: isDeleting } = useRequest({
mutationFn: () => {
return delDatasetById(datasetId);
},
onSuccess() {
router.replace(`/dataset/list`);
},
successToast: t('common:common.Delete Success'),
errorToast: t('common:common.Delete Failed')
});
const { runAsync: onSave, loading: isSaving } = useRequest2(
(data: DatasetItemType) => { (data: DatasetItemType) => {
return updateDataset({ return updateDataset({
id: datasetId, id: datasetId,
agentModel: data.agentModel, agentModel: data.agentModel,
externalReadUrl: data.externalReadUrl, externalReadUrl: data.externalReadUrl
defaultPermission: data.defaultPermission
}); });
}, },
{ {
@@ -101,7 +80,7 @@ const Info = ({ datasetId }: { datasetId: string }) => {
} }
); );
const { runAsync: onSelectFile, loading: isSelecting } = useRequest2( const { runAsync: onSelectFile } = useRequest2(
(e: File[]) => { (e: File[]) => {
const file = e[0]; const file = e[0];
if (!file) return Promise.resolve(null); if (!file) return Promise.resolve(null);
@@ -122,7 +101,7 @@ const Info = ({ datasetId }: { datasetId: string }) => {
} }
); );
const { runAsync: onRebuilding, loading: isRebuilding } = useRequest2( const { runAsync: onRebuilding } = useRequest2(
(vectorModel: VectorModelItemType) => { (vectorModel: VectorModelItemType) => {
return postRebuildEmbedding({ return postRebuildEmbedding({
datasetId, datasetId,
@@ -242,10 +221,9 @@ const Info = ({ datasetId }: { datasetId: string }) => {
onchange={(e) => { onchange={(e) => {
const vectorModel = vectorModelList.find((item) => item.model === e); const vectorModel = vectorModelList.find((item) => item.model === e);
if (!vectorModel) return; if (!vectorModel) return;
return onOpenConfirmRebuild(() => { return onOpenConfirmRebuild(async () => {
return onRebuilding(vectorModel).then(() => { await onRebuilding(vectorModel);
setValue('vectorModel', vectorModel); setValue('vectorModel', vectorModel);
});
})(); })();
}} }}
/> />
@@ -326,20 +304,12 @@ const Info = ({ datasetId }: { datasetId: string }) => {
<FormLabel fontWeight={'500'} fontSize={'mini'} pb={3} userSelect={'none'}> <FormLabel fontWeight={'500'} fontSize={'mini'} pb={3} userSelect={'none'}>
{t('common:permission.Default permission')} {t('common:permission.Default permission')}
</FormLabel> </FormLabel>
<DefaultPermissionList
fontSize={'mini'}
per={defaultPermission}
defaultPer={DatasetDefaultPermissionVal}
onChange={(v) => {
setValue('defaultPermission', v);
return handleSubmit((data) => onSave({ ...data, defaultPermission: v }))();
}}
/>
</Box> </Box>
<Box py={4}> <Box py={4}>
<MemberManager <MemberManager
managePer={{ managePer={{
mode: 'all',
permission: datasetDetail.permission, permission: datasetDetail.permission,
onGetCollaboratorList: () => getCollaboratorList(datasetId), onGetCollaboratorList: () => getCollaboratorList(datasetId),
permissionList: DatasetPermissionList, permissionList: DatasetPermissionList,
@@ -348,11 +318,19 @@ const Info = ({ datasetId }: { datasetId: string }) => {
...body, ...body,
datasetId datasetId
}), }),
onDelOneCollaborator: (tmbId) => onDelOneCollaborator: async ({ groupId, tmbId }) => {
deleteDatasetCollaborators({ if (tmbId) {
datasetId, return deleteDatasetCollaborators({
tmbId datasetId,
}) tmbId
});
} else if (groupId) {
return deleteDatasetCollaborators({
datasetId,
groupId
});
}
}
}} }}
/> />
</Box> </Box>

View File

@@ -18,10 +18,7 @@ import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
import dynamic from 'next/dynamic'; import dynamic from 'next/dynamic';
import { useContextSelector } from 'use-context-selector'; import { useContextSelector } from 'use-context-selector';
import { DatasetsContext } from '../context'; import { DatasetsContext } from '../context';
import { import { DatasetPermissionList } from '@fastgpt/global/support/permission/dataset/constant';
DatasetDefaultPermissionVal,
DatasetPermissionList
} from '@fastgpt/global/support/permission/dataset/constant';
import ConfigPerModal from '@/components/support/permission/ConfigPerModal'; import ConfigPerModal from '@/components/support/permission/ConfigPerModal';
import { import {
deleteDatasetCollaborators, deleteDatasetCollaborators,
@@ -34,7 +31,6 @@ import MyBox from '@fastgpt/web/components/common/MyBox';
import { useI18n } from '@/web/context/I18n'; import { useI18n } from '@/web/context/I18n';
import { useTranslation } from 'next-i18next'; import { useTranslation } from 'next-i18next';
import { useUserStore } from '@/web/support/user/useUserStore'; import { useUserStore } from '@/web/support/user/useUserStore';
import { formatTimeToChatTime } from '@fastgpt/global/common/string/time';
import { useSystem } from '@fastgpt/web/hooks/useSystem'; import { useSystem } from '@fastgpt/web/hooks/useSystem';
import SideTag from './SideTag'; import SideTag from './SideTag';
@@ -42,7 +38,6 @@ const EditResourceModal = dynamic(() => import('@/components/common/Modal/EditRe
function List() { function List() {
const { setLoading } = useSystemStore(); const { setLoading } = useSystemStore();
const { toast } = useToast();
const { isPc } = useSystem(); const { isPc } = useSystem();
const { t } = useTranslation(); const { t } = useTranslation();
const { commonT } = useI18n(); const { commonT } = useI18n();
@@ -59,21 +54,32 @@ function List() {
folderDetail folderDetail
} = useContextSelector(DatasetsContext, (v) => v); } = useContextSelector(DatasetsContext, (v) => v);
const [editPerDatasetIndex, setEditPerDatasetIndex] = useState<number>(); const [editPerDatasetIndex, setEditPerDatasetIndex] = useState<number>();
const [loadingDatasetId, setLoadingDatasetId] = useState<string>(); const router = useRouter();
const { parentId = null } = router.query as { parentId?: string | null };
const parentDataset = useMemo(
() => myDatasets.find((item) => String(item._id) === parentId),
[parentId, myDatasets]
);
const { openConfirm: openMoveConfirm, ConfirmModal: MoveConfirmModal } = useConfirm({
type: 'common',
title: t('common:move.confirm'),
content: t('dataset:move.hint')
});
const { runAsync: updateDataset } = useRequest2(onUpdateDataset);
const { getBoxProps } = useFolderDrag({ const { getBoxProps } = useFolderDrag({
activeStyles: { activeStyles: {
borderColor: 'primary.600' borderColor: 'primary.600'
}, },
onDrop: async (dragId: string, targetId: string) => { onDrop: (dragId: string, targetId: string) => {
setLoadingDatasetId(dragId); openMoveConfirm(() =>
try { updateDataset({
await onUpdateDataset({
id: dragId, id: dragId,
parentId: targetId parentId: targetId
}); })
} catch (error) {} )();
setLoadingDatasetId(undefined);
} }
}); });
@@ -86,10 +92,6 @@ function List() {
[editPerDatasetIndex, myDatasets] [editPerDatasetIndex, myDatasets]
); );
const router = useRouter();
const { parentId = null } = router.query as { parentId?: string | null };
const { mutate: exportDataset } = useRequest({ const { mutate: exportDataset } = useRequest({
mutationFn: async (dataset: DatasetItemType) => { mutationFn: async (dataset: DatasetItemType) => {
setLoading(true); setLoading(true);
@@ -100,15 +102,10 @@ function List() {
filename: `${dataset.name}.csv` filename: `${dataset.name}.csv`
}); });
}, },
onSuccess() {
toast({
status: 'success',
title: t('common:core.dataset.Start export')
});
},
onSettled() { onSettled() {
setLoading(false); setLoading(false);
}, },
successToast: t('common:core.dataset.Start export'),
errorToast: t('common:dataset.Export Dataset Limit Error') errorToast: t('common:dataset.Export Dataset Limit Error')
}); });
@@ -176,7 +173,6 @@ function List() {
} }
> >
<MyBox <MyBox
isLoading={loadingDatasetId === dataset._id}
display={'flex'} display={'flex'}
flexDirection={'column'} flexDirection={'column'}
lineHeight={1.5} lineHeight={1.5}
@@ -278,8 +274,8 @@ function List() {
</HStack> </HStack>
)} )}
<PermissionIconText <PermissionIconText
private={dataset.private}
iconColor="myGray.400" iconColor="myGray.400"
defaultPermission={dataset.defaultPermission}
color={'myGray.500'} color={'myGray.500'}
/> />
</HStack> </HStack>
@@ -293,7 +289,9 @@ function List() {
</Box> </Box>
</HStack> </HStack>
)} )}
{dataset.permission.hasWritePer && ( {(dataset.type === DatasetTypeEnum.folder
? dataset.permission.hasManagePer
: dataset.permission.hasWritePer) && (
<Box <Box
className="more" className="more"
display={['', 'none']} display={['', 'none']}
@@ -336,11 +334,18 @@ function List() {
avatar: dataset.avatar avatar: dataset.avatar
}) })
}, },
{ ...((parentDataset ? parentDataset : dataset)?.permission
icon: 'common/file/move', .hasManagePer
label: t('common:Move'), ? [
onClick: () => setMoveDatasetId(dataset._id) {
}, icon: 'common/file/move',
label: t('common:Move'),
onClick: () => {
setMoveDatasetId(dataset._id);
}
}
]
: []),
...(dataset.permission.hasManagePer ...(dataset.permission.hasManagePer
? [ ? [
{ {
@@ -427,36 +432,20 @@ function List() {
} }
avatar={editPerDataset.avatar} avatar={editPerDataset.avatar}
name={editPerDataset.name} name={editPerDataset.name}
defaultPer={{
value: editPerDataset.defaultPermission,
defaultValue: DatasetDefaultPermissionVal,
onChange: (e) =>
onUpdateDataset({
id: editPerDataset._id,
defaultPermission: e
})
}}
managePer={{ managePer={{
mode: 'all',
permission: editPerDataset.permission, permission: editPerDataset.permission,
onGetCollaboratorList: () => getCollaboratorList(editPerDataset._id), onGetCollaboratorList: () => getCollaboratorList(editPerDataset._id),
permissionList: DatasetPermissionList, permissionList: DatasetPermissionList,
onUpdateCollaborators: ({ onUpdateCollaborators: (props) =>
members = [], // TODO: remove default value after group is ready postUpdateDatasetCollaborators({
permission ...props,
}: {
members?: string[];
permission: number;
}) => {
return postUpdateDatasetCollaborators({
members,
permission,
datasetId: editPerDataset._id datasetId: editPerDataset._id
}); }),
}, onDelOneCollaborator: async (props) =>
onDelOneCollaborator: (tmbId: string) =>
deleteDatasetCollaborators({ deleteDatasetCollaborators({
datasetId: editPerDataset._id, ...props,
tmbId datasetId: editPerDataset._id
}), }),
refreshDeps: [editPerDataset._id, editPerDataset.inheritPermission] refreshDeps: [editPerDataset._id, editPerDataset.inheritPermission]
}} }}
@@ -464,6 +453,7 @@ function List() {
/> />
)} )}
<ConfirmModal /> <ConfirmModal />
<MoveConfirmModal />
</> </>
); );
} }

View File

@@ -1,186 +0,0 @@
import React, { useMemo, useState } from 'react';
import {
Card,
Flex,
Box,
Button,
ModalBody,
ModalHeader,
ModalFooter,
useTheme,
Grid
} from '@chakra-ui/react';
import Avatar from '@fastgpt/web/components/common/Avatar';
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
import MyModal from '@fastgpt/web/components/common/MyModal';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { DatasetTypeEnum } from '@fastgpt/global/core/dataset/constants';
import { useTranslation } from 'next-i18next';
import { useQuery } from '@tanstack/react-query';
import { getDatasets, putDatasetById, getDatasetPaths } from '@/web/core/dataset/api';
import { useRequest } from '@fastgpt/web/hooks/useRequest';
import EmptyTip from '@fastgpt/web/components/common/EmptyTip';
const MoveModal = ({
onClose,
onSuccess,
moveDataId
}: {
onClose: () => void;
onSuccess: () => void;
moveDataId: string;
}) => {
const { t } = useTranslation();
const theme = useTheme();
const [parentId, setParentId] = useState<string>('');
const { data } = useQuery(['getDatasets', parentId], () => {
return Promise.all([
getDatasets({ parentId, type: DatasetTypeEnum.folder }),
getDatasetPaths(parentId)
]);
});
const paths = useMemo(
() => [
{
parentId: '',
parentName: t('common:core.dataset.My Dataset')
},
...(data?.[1] || [])
],
[data, t]
);
const folderList = useMemo(
() => (data?.[0] || []).filter((item) => item._id !== moveDataId),
[moveDataId, data]
);
const { mutate, isLoading } = useRequest({
mutationFn: () => putDatasetById({ id: moveDataId, parentId }),
onSuccess,
errorToast: t('common:dataset.Move Failed')
});
return (
<MyModal
isOpen={true}
maxW={['90vw', '800px']}
w={'800px'}
iconSrc="/imgs/modal/move.svg"
title={
<>
{!!parentId ? (
<Flex flex={1} userSelect={'none'} fontSize={['sm', 'md']} fontWeight={'normal'}>
{paths.map((item, i) => (
<Flex key={item.parentId} mr={2} alignItems={'center'}>
<Box
borderRadius={'md'}
{...(i === paths.length - 1
? {
cursor: 'default'
}
: {
cursor: 'pointer',
_hover: {
color: 'primary.500'
},
onClick: () => {
setParentId(item.parentId);
}
})}
>
{item.parentName}
</Box>
{i !== paths.length - 1 && (
<MyIcon name={'common/rightArrowLight'} color={'myGray.500'} w={'14px'} />
)}
</Flex>
))}
</Flex>
) : (
<Box>{t('common:core.dataset.My Dataset')}</Box>
)}
</>
}
onClose={onClose}
>
<Flex flexDirection={'column'} h={['90vh', 'auto']}>
<ModalBody
flex={['1 0 0', '0 0 auto']}
maxH={'80vh'}
overflowY={'auto'}
display={'grid'}
userSelect={'none'}
>
<Grid
gridTemplateColumns={['repeat(1,1fr)', 'repeat(2,1fr)', 'repeat(3,1fr)']}
gridGap={3}
>
{folderList.map((item) =>
(() => {
return (
<MyTooltip
key={item._id}
label={
item.type === DatasetTypeEnum.dataset
? t('common:dataset.Select Dataset')
: t('common:dataset.Select Folder')
}
>
<Card
p={3}
border={theme.borders.base}
boxShadow={'sm'}
h={'80px'}
cursor={'pointer'}
_hover={{
boxShadow: 'md'
}}
onClick={() => {
setParentId(item._id);
}}
>
<Flex alignItems={'center'} h={'38px'}>
<Avatar src={item.avatar} w={['24px', '28px']}></Avatar>
<Box
className="textEllipsis"
ml={3}
fontWeight={'bold'}
fontSize={['md', 'md']}
>
{item.name}
</Box>
</Flex>
<Flex justifyContent={'flex-end'} alignItems={'center'} fontSize={'sm'}>
{item.type === DatasetTypeEnum.folder ? (
<Box color={'myGray.500'}>{t('common:Folder')}</Box>
) : (
<>
<MyIcon mr={1} name="kbTest" w={'12px'} />
<Box color={'myGray.500'}>{item.vectorModel.name}</Box>
</>
)}
</Flex>
</Card>
</MyTooltip>
);
})()
)}
</Grid>
{folderList.length === 0 && (
<EmptyTip text={t('common:common.folder.No Folder')}></EmptyTip>
)}
</ModalBody>
<ModalFooter>
<Button isLoading={isLoading} onClick={mutate}>
{t('common:dataset.Confirm move the folder')}
</Button>
</ModalFooter>
</Flex>
</MyModal>
);
};
export default MoveModal;

View File

@@ -13,7 +13,6 @@ import {
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import React, { useCallback, useState } from 'react'; import React, { useCallback, useState } from 'react';
import { createContext } from 'use-context-selector'; import { createContext } from 'use-context-selector';
import { useI18n } from '@/web/context/I18n';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest'; import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { DatasetUpdateBody } from '@fastgpt/global/core/dataset/api'; import { DatasetUpdateBody } from '@fastgpt/global/core/dataset/api';
import dynamic from 'next/dynamic'; import dynamic from 'next/dynamic';
@@ -68,7 +67,6 @@ export const DatasetsContext = createContext<DatasetContextType>({
function DatasetContextProvider({ children }: { children: React.ReactNode }) { function DatasetContextProvider({ children }: { children: React.ReactNode }) {
const router = useRouter(); const router = useRouter();
const { commonT } = useI18n();
const { t } = useTranslation(); const { t } = useTranslation();
const [moveDatasetId, setMoveDatasetId] = useState<string>(); const [moveDatasetId, setMoveDatasetId] = useState<string>();
const [searchKey, setSearchKey] = useState(''); const [searchKey, setSearchKey] = useState('');
@@ -127,10 +125,12 @@ function DatasetContextProvider({ children }: { children: React.ReactNode }) {
parentId, parentId,
type: DatasetTypeEnum.folder type: DatasetTypeEnum.folder
}) })
).map((item) => ({ )
id: item._id, .filter((item) => item.permission.hasManagePer)
name: item.name .map((item) => ({
})); id: item._id,
name: item.name
}));
}, []); }, []);
const [editedDataset, setEditedDataset] = useState<EditResourceInfoFormType>(); const [editedDataset, setEditedDataset] = useState<EditResourceInfoFormType>();
@@ -164,9 +164,10 @@ function DatasetContextProvider({ children }: { children: React.ReactNode }) {
<MoveModal <MoveModal
moveResourceId={moveDatasetId} moveResourceId={moveDatasetId}
server={getDatasetFolderList} server={getDatasetFolderList}
title={commonT('Move')} title={t('common:Move')}
onClose={() => setMoveDatasetId(undefined)} onClose={() => setMoveDatasetId(undefined)}
onConfirm={onMoveDataset} onConfirm={(parentId) => onMoveDataset(parentId)}
moveHint={t('dataset:move.hint')}
/> />
)} )}
</DatasetsContext.Provider> </DatasetsContext.Provider>

View File

@@ -17,10 +17,7 @@ import { EditFolderFormType } from '@fastgpt/web/components/common/MyModal/EditF
import dynamic from 'next/dynamic'; import dynamic from 'next/dynamic';
import { postCreateDatasetFolder, resumeInheritPer } from '@/web/core/dataset/api'; import { postCreateDatasetFolder, resumeInheritPer } from '@/web/core/dataset/api';
import FolderSlideCard from '@/components/common/folder/SlideCard'; import FolderSlideCard from '@/components/common/folder/SlideCard';
import { import { DatasetPermissionList } from '@fastgpt/global/support/permission/dataset/constant';
DatasetDefaultPermissionVal,
DatasetPermissionList
} from '@fastgpt/global/support/permission/dataset/constant';
import { import {
postUpdateDatasetCollaborators, postUpdateDatasetCollaborators,
deleteDatasetCollaborators, deleteDatasetCollaborators,
@@ -52,7 +49,6 @@ const Dataset = () => {
loadMyDatasets, loadMyDatasets,
refetchFolderDetail, refetchFolderDetail,
folderDetail, folderDetail,
setEditedDataset,
setMoveDatasetId, setMoveDatasetId,
onDelDataset, onDelDataset,
onUpdateDataset, onUpdateDataset,
@@ -228,38 +224,39 @@ const Dataset = () => {
}); });
}) })
} }
defaultPer={{
value: folderDetail.defaultPermission,
defaultValue: DatasetDefaultPermissionVal,
onChange: (e) => {
return onUpdateDataset({
id: folderDetail._id,
defaultPermission: e
});
}
}}
managePer={{ managePer={{
mode: 'all',
permission: folderDetail.permission, permission: folderDetail.permission,
onGetCollaboratorList: () => getCollaboratorList(folderDetail._id), onGetCollaboratorList: () => getCollaboratorList(folderDetail._id),
permissionList: DatasetPermissionList, permissionList: DatasetPermissionList,
onUpdateCollaborators: ({ onUpdateCollaborators: ({
members = [], // TODO: remove the default value after group is ready members,
groups,
permission permission
}: { }: {
members?: string[]; members?: string[];
groups?: string[];
permission: number; permission: number;
}) => { }) =>
return postUpdateDatasetCollaborators({ postUpdateDatasetCollaborators({
members, members,
groups,
permission, permission,
datasetId: folderDetail._id datasetId: folderDetail._id
});
},
onDelOneCollaborator: (tmbId: string) =>
deleteDatasetCollaborators({
datasetId: folderDetail._id,
tmbId
}), }),
onDelOneCollaborator: async ({ tmbId, groupId }) => {
if (tmbId) {
return deleteDatasetCollaborators({
datasetId: folderDetail._id,
tmbId
});
} else if (groupId) {
return deleteDatasetCollaborators({
datasetId: folderDetail._id,
groupId
});
}
},
refreshDeps: [folderDetail._id, folderDetail.inheritPermission] refreshDeps: [folderDetail._id, folderDetail.inheritPermission]
}} }}
/> />

View File

@@ -64,7 +64,7 @@ export function getTestRequest<Q = any, B = any>({
]; ];
} }
export const MockParseHeaderCert = async ({ export const parseHeaderCertMock = async ({
req, req,
authToken = true, authToken = true,
authRoot = false, authRoot = false,

View File

@@ -3,6 +3,7 @@ export const getDocPath = (path: string) => {
const feConfigs = useSystemStore.getState().feConfigs; const feConfigs = useSystemStore.getState().feConfigs;
if (!feConfigs?.docUrl) return ''; if (!feConfigs?.docUrl) return '';
if (!path.startsWith('/')) return path;
if (feConfigs.docUrl.endsWith('/')) return feConfigs.docUrl.slice(0, -1); if (feConfigs.docUrl.endsWith('/')) return feConfigs.docUrl.slice(0, -1);
return feConfigs.docUrl + path; return feConfigs.docUrl + path;
}; };

View File

@@ -2,8 +2,6 @@ import { AppTypeEnum } from '@fastgpt/global/core/app/constants';
import { AppDetailType } from '@fastgpt/global/core/app/type.d'; import { AppDetailType } from '@fastgpt/global/core/app/type.d';
import type { FeishuAppType, OutLinkEditType } from '@fastgpt/global/support/outLink/type.d'; import type { FeishuAppType, OutLinkEditType } from '@fastgpt/global/support/outLink/type.d';
import { AppPermission } from '@fastgpt/global/support/permission/app/controller'; import { AppPermission } from '@fastgpt/global/support/permission/app/controller';
import { NullPermission } from '@fastgpt/global/support/permission/constant';
import { i18nT } from '@fastgpt/web/i18n/utils';
export const defaultApp: AppDetailType = { export const defaultApp: AppDetailType = {
_id: '', _id: '',
name: 'AI', name: 'AI',
@@ -18,7 +16,6 @@ export const defaultApp: AppDetailType = {
teamTags: [], teamTags: [],
edges: [], edges: [],
version: 'v2', version: 'v2',
defaultPermission: NullPermission,
permission: new AppPermission(), permission: new AppPermission(),
inheritPermission: false inheritPermission: false
}; };

View File

@@ -11,5 +11,5 @@ export const getCollaboratorList = (datasetId: string) =>
export const postUpdateDatasetCollaborators = (body: UpdateDatasetCollaboratorBody) => export const postUpdateDatasetCollaborators = (body: UpdateDatasetCollaboratorBody) =>
POST('/proApi/core/dataset/collaborator/update', body); POST('/proApi/core/dataset/collaborator/update', body);
export const deleteDatasetCollaborators = ({ ...params }: DatasetCollaboratorDeleteParams) => export const deleteDatasetCollaborators = (params: DatasetCollaboratorDeleteParams) =>
DELETE('/proApi/core/dataset/collaborator/delete', { ...params }); DELETE('/proApi/core/dataset/collaborator/delete', params);

View File

@@ -8,7 +8,6 @@ import type {
DatasetCollectionItemType, DatasetCollectionItemType,
DatasetItemType DatasetItemType
} from '@fastgpt/global/core/dataset/type.d'; } from '@fastgpt/global/core/dataset/type.d';
import { DatasetDefaultPermissionVal } from '@fastgpt/global/support/permission/dataset/constant';
import { DatasetPermission } from '@fastgpt/global/support/permission/dataset/controller'; import { DatasetPermission } from '@fastgpt/global/support/permission/dataset/controller';
export const defaultDatasetDetail: DatasetItemType = { export const defaultDatasetDetail: DatasetItemType = {
@@ -26,7 +25,6 @@ export const defaultDatasetDetail: DatasetItemType = {
permission: new DatasetPermission(), permission: new DatasetPermission(),
vectorModel: defaultVectorModels[0], vectorModel: defaultVectorModels[0],
agentModel: defaultQAModels[0], agentModel: defaultQAModels[0],
defaultPermission: DatasetDefaultPermissionVal,
inheritPermission: true inheritPermission: true
}; };
@@ -48,7 +46,6 @@ export const defaultCollectionDetail: DatasetCollectionItemType = {
status: 'active', status: 'active',
vectorModel: defaultVectorModels[0].model, vectorModel: defaultVectorModels[0].model,
agentModel: defaultQAModels[0].model, agentModel: defaultQAModels[0].model,
defaultPermission: DatasetDefaultPermissionVal,
inheritPermission: true inheritPermission: true
}, },
tags: [], tags: [],

View File

@@ -193,10 +193,11 @@ export const computedNodeInputReference = ({
if (!node) { if (!node) {
return; return;
} }
const parentId = node.parentNodeId;
let sourceNodes: FlowNodeItemType[] = []; let sourceNodes: FlowNodeItemType[] = [];
// 根据 edge 获取所有的 source 节点source节点会继续向前递归获取 // 根据 edge 获取所有的 source 节点source节点会继续向前递归获取
const findSourceNode = (nodeId: string) => { const findSourceNode = (nodeId: string) => {
const targetEdges = edges.filter((item) => item.target === nodeId); const targetEdges = edges.filter((item) => item.target === nodeId || item.target === parentId);
targetEdges.forEach((edge) => { targetEdges.forEach((edge) => {
const sourceNode = nodes.find((item) => item.nodeId === edge.source); const sourceNode = nodes.find((item) => item.nodeId === edge.source);
if (!sourceNode) return; if (!sourceNode) return;

View File

@@ -28,6 +28,7 @@ type State = {
loadAndGetTeamMembers: (init?: boolean) => Promise<TeamMemberItemType[]>; loadAndGetTeamMembers: (init?: boolean) => Promise<TeamMemberItemType[]>;
teamMemberGroups: MemberGroupListType; teamMemberGroups: MemberGroupListType;
myGroups: MemberGroupListType;
loadAndGetGroups: (init?: boolean) => Promise<MemberGroupListType>; loadAndGetGroups: (init?: boolean) => Promise<MemberGroupListType>;
}; };
@@ -106,6 +107,7 @@ export const useUserStore = create<State>()(
return res; return res;
}, },
teamMemberGroups: [], teamMemberGroups: [],
myGroups: [],
loadAndGetGroups: async (init = false) => { loadAndGetGroups: async (init = false) => {
if (!useSystemStore.getState()?.feConfigs?.isPlus) return []; if (!useSystemStore.getState()?.feConfigs?.isPlus) return [];
@@ -116,6 +118,9 @@ export const useUserStore = create<State>()(
const res = await getGroupList(); const res = await getGroupList();
set((state) => { set((state) => {
state.teamMemberGroups = res; state.teamMemberGroups = res;
state.myGroups = res.filter((item) =>
item.members.map((i) => String(i.tmbId)).includes(String(state.userInfo?.team?.tmbId))
);
}); });
return res; return res;