diff --git a/document/content/docs/introduction/development/openapi/dataset.mdx b/document/content/docs/introduction/development/openapi/dataset.mdx index 46df17a352ab..9dd99acac312 100644 --- a/document/content/docs/introduction/development/openapi/dataset.mdx +++ b/document/content/docs/introduction/development/openapi/dataset.mdx @@ -868,8 +868,12 @@ curl --location --request PUT 'http://localhost:3000/api/core/dataset/collection ```bash -curl --location --request DELETE 'http://localhost:3000/api/core/dataset/collection/delete?id=65aa2a64e6cb9b8ccdc00de8' \ ---header 'Authorization: Bearer {{authorization}}' \ +curl --location --request POST 'http://localhost:3000/api/core/dataset/collection/delete' \ +--header 'Authorization: Bearer fastgpt-' \ +--header 'Content-Type: application/json' \ +--data-raw '{ + "collectionIds": ["65a8cdcb0d70d3de0bf08d0a"] +}' ``` @@ -877,7 +881,7 @@ curl --location --request DELETE 'http://localhost:3000/api/core/dataset/collect
-- id: 集合的ID +- collectionIds: 集合的 ID 列表
diff --git a/document/content/docs/toc.mdx b/document/content/docs/toc.mdx index 5e1659ad92d2..a49c20408046 100644 --- a/document/content/docs/toc.mdx +++ b/document/content/docs/toc.mdx @@ -102,6 +102,7 @@ description: FastGPT 文档目录 - [/docs/upgrading/4-12/4121](/docs/upgrading/4-12/4121) - [/docs/upgrading/4-12/4122](/docs/upgrading/4-12/4122) - [/docs/upgrading/4-12/4123](/docs/upgrading/4-12/4123) +- [/docs/upgrading/4-12/4124](/docs/upgrading/4-12/4124) - [/docs/upgrading/4-8/40](/docs/upgrading/4-8/40) - [/docs/upgrading/4-8/41](/docs/upgrading/4-8/41) - [/docs/upgrading/4-8/42](/docs/upgrading/4-8/42) @@ -180,3 +181,4 @@ description: FastGPT 文档目录 - [/docs/use-cases/external-integration/feishu](/docs/use-cases/external-integration/feishu) - [/docs/use-cases/external-integration/official_account](/docs/use-cases/external-integration/official_account) - [/docs/use-cases/external-integration/openapi](/docs/use-cases/external-integration/openapi) +- [/docs/use-cases/external-integration/wecom](/docs/use-cases/external-integration/wecom) diff --git a/document/content/docs/upgrading/4-12/4124.mdx b/document/content/docs/upgrading/4-12/4124.mdx new file mode 100644 index 000000000000..7ef63c0ec2de --- /dev/null +++ b/document/content/docs/upgrading/4-12/4124.mdx @@ -0,0 +1,26 @@ +--- +title: 'V4.12.4(进行)' +description: 'FastGPT V4.12.4 更新说明' +--- + +## 🚀 新增内容 + +1. 商业版支持企微发布渠道。 + +## ⚙️ 优化 + +1. 权限继承优化,子资源权限高于父级时,不会强制打断继承模式。 +2. Prompt 编辑器支持列表渲染。 +3. 数据页返回知识库列表,保持分页。 +4. 知识库上传文件成功后,返回对应上传目录。 +5. 删除应用,减少事务操作。 +6. 用户选择 UI。 + +## 🐛 修复 + +1. HTTP 工具空指针,导致无法编辑。 +2. python 代码运行,入参无法是 boolean 值。 +3. debug 模式下,全局变量未传递。 + +## 🔨 插件更新 + diff --git a/document/content/docs/upgrading/4-12/meta.json b/document/content/docs/upgrading/4-12/meta.json index 72d8005d2a46..30c62716d0d5 100644 --- a/document/content/docs/upgrading/4-12/meta.json +++ b/document/content/docs/upgrading/4-12/meta.json @@ -1,5 +1,5 @@ { "title": "4.12.x", "description": "", - "pages": ["4123", "4122", "4121", "4120"] + "pages": ["4124", "4123", "4122", "4121", "4120"] } diff --git a/document/content/docs/use-cases/external-integration/meta.json b/document/content/docs/use-cases/external-integration/meta.json index 2de5862b29bc..996ab69464dc 100644 --- a/document/content/docs/use-cases/external-integration/meta.json +++ b/document/content/docs/use-cases/external-integration/meta.json @@ -1,5 +1,5 @@ { "title": "外部调用 FastGPT", "description": "外部应用通过多种方式调用 FastGPT 功能的教程", - "pages": ["openapi", "feishu", "dingtalk", "official_account"] + "pages": ["openapi", "feishu", "dingtalk", "wecom", "official_account"] } diff --git a/document/content/docs/use-cases/external-integration/wecom.mdx b/document/content/docs/use-cases/external-integration/wecom.mdx new file mode 100644 index 000000000000..b68d8d988ff6 --- /dev/null +++ b/document/content/docs/use-cases/external-integration/wecom.mdx @@ -0,0 +1,112 @@ +--- +title: 接入企微机器人教程 +description: FastGPT 接入企微机器人教程 +--- + +从 4.12.4 版本起,FastGPT 商业版支持直接接入企微机器人,无需额外的 API。 + +## 1.配置可信域名和可信IP + +点击企微头像,打开管理企业 + +![图片](/imgs/wecom-bot-1.png) + +在应用管理中找到"自建"-"创建应用" + +![图片](/imgs/wecom-bot-2.png) + +创建好应用后, 下拉, 依次配置"网页授权及JS-SDK"和"企业可信IP" + +![图片](/imgs/wecom-bot-3.png) + +其中, 网页授权及JS-SDK要求按照企微指引,完成域名归属认证 + +![图片](/imgs/wecom-bot-4.png) + +企业可信IP要求为企业服务器IP, 后续企微的回调URL将请求到此IP + +![图片](/imgs/wecom-bot-5.png) + +## 2. 创建企业自建应用 + +前往 FastGPT ,选择想要接入的应用,在 发布渠道 页面,新建一个接入企微智能机器人的发布渠道,填写好基础信息。 + +![图片](/imgs/wecom-bot-6.png) + +现在回到企业微信平台,找到 Corp ID, Agent ID, Token, AES Key 信息并填写回 FastGPT 平台 + +![图片](/imgs/wecom-bot-7.png) + +在"我的企业"里找到企业 ID, 填写到 FastGPT 的 Corp ID 中 + +![图片](/imgs/wecom-bot-8.png) + +在应用中找到 Agent Id 和 Secret, 并填写回 FastGPT + +![图片](/imgs/wecom-bot-9.png) + +点击"消息接收"-"设置API接收" + +![图片](/imgs/wecom-bot-10.png) + +随机生成或者手动输入 Token 和 Encoding-Key, 分别填写到 FastGPT 的 Token 和 AES Key 中 + +![图片](/imgs/wecom-bot-11.png) + +填写完成后确认创建 + +然后点击请求地址, 复制页面中的链接 + +![图片](/imgs/wecom-bot-12.png) + +回到刚才的配置详情, 将刚才复制的链接填入 URL 框中, 并点击创建 + +注意: 若复制的链接是以 "http://localhost" 开头, 需要将本地地址改为企业主体域名 + +因为企微会给填写的 URL 发送验证密文, 若 URL 为本地地址, 则本地接收不到企微的密文 + +若 URL 不是企业主体域名, 则验证会失败 + +## 3. 创建智能机器人 + +在"安全与管理" - "管理工具"页面找到"智能机器人" ( 注意: 只有企业创建者或超级管理员才有权限看到这个入口 ) + +![图片](/imgs/wecom-bot-13.png) + +创建机器人页面,下拉,找到,点击"API模式创建" + +![图片](/imgs/wecom-bot-14.png) + +与刚才配置自建应用同理, 配置这三个参数 + +![图片](/imgs/wecom-bot-15.png) + +注意: 这里的 Agent ID , 和上面的不同, 可以先随意填写一个值, 后续会根据企业微信提供的数据重新更改 + +Secret 为用户自己决定的密令 + +填写完成后确认创建 + +然后点击请求地址, 复制页面中的链接, 链接的地址也必须为企业主体域名 + +创建完成后, 找到智能机器人的配置详情 + +![图片](/imgs/wecom-bot-16.png) + +复制 Bot ID, 填写到 FastGPT 的 Agent ID 中 + +![图片](/imgs/wecom-bot-17.png) + +## 4. 使用智能机器人 + +在企业微信平台的"通讯录",即可找到创建的机器人,就可以发送消息了 + +![图片](/imgs/wecom-bot-18.png) + +## FAQ + +### 发送了消息,没响应 + +1. 检查企微机器人回调地址、权限等是否正确。 +2. 查看 FastGPT 对话日志,是否有对应的提问记录 +3. 如果没记录,则可能是应用运行报错了,可以先试试最简单的机器人。(飞书机器人无法输入全局变量、文件、图片内容) diff --git a/document/data/doc-last-modified.json b/document/data/doc-last-modified.json index c93efb846477..0dc2b17e6e4b 100644 --- a/document/data/doc-last-modified.json +++ b/document/data/doc-last-modified.json @@ -31,7 +31,7 @@ "document/content/docs/introduction/development/modelConfig/ppio.mdx": "2025-08-05T23:20:39+08:00", "document/content/docs/introduction/development/modelConfig/siliconCloud.mdx": "2025-08-05T23:20:39+08:00", "document/content/docs/introduction/development/openapi/chat.mdx": "2025-08-14T18:54:47+08:00", - "document/content/docs/introduction/development/openapi/dataset.mdx": "2025-08-14T18:54:47+08:00", + "document/content/docs/introduction/development/openapi/dataset.mdx": "2025-09-11T10:29:11+08:00", "document/content/docs/introduction/development/openapi/intro.mdx": "2025-08-14T18:54:47+08:00", "document/content/docs/introduction/development/openapi/share.mdx": "2025-08-05T23:20:39+08:00", "document/content/docs/introduction/development/proxy/cloudflare.mdx": "2025-07-23T21:35:03+08:00", @@ -40,7 +40,7 @@ "document/content/docs/introduction/development/sealos.mdx": "2025-08-05T23:20:39+08:00", "document/content/docs/introduction/guide/DialogBoxes/htmlRendering.mdx": "2025-07-23T21:35:03+08:00", "document/content/docs/introduction/guide/DialogBoxes/quoteList.mdx": "2025-07-23T21:35:03+08:00", - "document/content/docs/introduction/guide/admin/sso.mdx": "2025-07-24T13:00:27+08:00", + "document/content/docs/introduction/guide/admin/sso.mdx": "2025-09-08T20:07:04+08:00", "document/content/docs/introduction/guide/admin/teamMode.mdx": "2025-08-27T16:59:57+08:00", "document/content/docs/introduction/guide/course/ai_settings.mdx": "2025-07-24T13:00:27+08:00", "document/content/docs/introduction/guide/course/chat_input_guide.mdx": "2025-07-23T21:35:03+08:00", @@ -97,15 +97,16 @@ "document/content/docs/protocol/terms.en.mdx": "2025-08-03T22:37:45+08:00", "document/content/docs/protocol/terms.mdx": "2025-08-03T22:37:45+08:00", "document/content/docs/toc.en.mdx": "2025-08-04T13:42:36+08:00", - "document/content/docs/toc.mdx": "2025-08-29T01:24:19+08:00", + "document/content/docs/toc.mdx": "2025-09-12T12:58:39+08:00", "document/content/docs/upgrading/4-10/4100.mdx": "2025-08-02T19:38:37+08:00", - "document/content/docs/upgrading/4-10/4101.mdx": "2025-08-02T19:38:37+08:00", + "document/content/docs/upgrading/4-10/4101.mdx": "2025-09-08T20:07:20+08:00", "document/content/docs/upgrading/4-11/4110.mdx": "2025-08-05T23:20:39+08:00", "document/content/docs/upgrading/4-11/4111.mdx": "2025-08-07T22:49:09+08:00", "document/content/docs/upgrading/4-12/4120.mdx": "2025-09-07T14:41:48+08:00", "document/content/docs/upgrading/4-12/4121.mdx": "2025-09-07T14:41:48+08:00", "document/content/docs/upgrading/4-12/4122.mdx": "2025-09-07T14:41:48+08:00", - "document/content/docs/upgrading/4-12/4123.mdx": "2025-09-07T14:41:48+08:00", + "document/content/docs/upgrading/4-12/4123.mdx": "2025-09-07T20:55:14+08:00", + "document/content/docs/upgrading/4-12/4124.mdx": "2025-09-13T01:34:04+08:00", "document/content/docs/upgrading/4-8/40.mdx": "2025-08-02T19:38:37+08:00", "document/content/docs/upgrading/4-8/41.mdx": "2025-08-02T19:38:37+08:00", "document/content/docs/upgrading/4-8/42.mdx": "2025-08-02T19:38:37+08:00", @@ -185,5 +186,6 @@ "document/content/docs/use-cases/external-integration/feishu.mdx": "2025-07-24T14:23:04+08:00", "document/content/docs/use-cases/external-integration/official_account.mdx": "2025-08-05T23:20:39+08:00", "document/content/docs/use-cases/external-integration/openapi.mdx": "2025-08-14T18:54:47+08:00", + "document/content/docs/use-cases/external-integration/wecom.mdx": "2025-09-12T12:58:39+08:00", "document/content/docs/use-cases/index.mdx": "2025-07-24T14:23:04+08:00" } \ No newline at end of file diff --git a/document/public/imgs/wecom-bot-1.png b/document/public/imgs/wecom-bot-1.png new file mode 100644 index 000000000000..1be6328206ac Binary files /dev/null and b/document/public/imgs/wecom-bot-1.png differ diff --git a/document/public/imgs/wecom-bot-10.png b/document/public/imgs/wecom-bot-10.png new file mode 100644 index 000000000000..6f2ee2be84c5 Binary files /dev/null and b/document/public/imgs/wecom-bot-10.png differ diff --git a/document/public/imgs/wecom-bot-11.png b/document/public/imgs/wecom-bot-11.png new file mode 100644 index 000000000000..5b49a5bc4966 Binary files /dev/null and b/document/public/imgs/wecom-bot-11.png differ diff --git a/document/public/imgs/wecom-bot-12.png b/document/public/imgs/wecom-bot-12.png new file mode 100644 index 000000000000..136ce94bb61e Binary files /dev/null and b/document/public/imgs/wecom-bot-12.png differ diff --git a/document/public/imgs/wecom-bot-13.png b/document/public/imgs/wecom-bot-13.png new file mode 100644 index 000000000000..feb7668c29c2 Binary files /dev/null and b/document/public/imgs/wecom-bot-13.png differ diff --git a/document/public/imgs/wecom-bot-14.png b/document/public/imgs/wecom-bot-14.png new file mode 100644 index 000000000000..0a956f2adbd1 Binary files /dev/null and b/document/public/imgs/wecom-bot-14.png differ diff --git a/document/public/imgs/wecom-bot-15.png b/document/public/imgs/wecom-bot-15.png new file mode 100644 index 000000000000..a67f2f1ecdc2 Binary files /dev/null and b/document/public/imgs/wecom-bot-15.png differ diff --git a/document/public/imgs/wecom-bot-16.png b/document/public/imgs/wecom-bot-16.png new file mode 100644 index 000000000000..e3f06ab3399a Binary files /dev/null and b/document/public/imgs/wecom-bot-16.png differ diff --git a/document/public/imgs/wecom-bot-17.png b/document/public/imgs/wecom-bot-17.png new file mode 100644 index 000000000000..c886e3557b86 Binary files /dev/null and b/document/public/imgs/wecom-bot-17.png differ diff --git a/document/public/imgs/wecom-bot-18.png b/document/public/imgs/wecom-bot-18.png new file mode 100644 index 000000000000..249144a06b2e Binary files /dev/null and b/document/public/imgs/wecom-bot-18.png differ diff --git a/document/public/imgs/wecom-bot-2.png b/document/public/imgs/wecom-bot-2.png new file mode 100644 index 000000000000..b987d4cc0a50 Binary files /dev/null and b/document/public/imgs/wecom-bot-2.png differ diff --git a/document/public/imgs/wecom-bot-3.png b/document/public/imgs/wecom-bot-3.png new file mode 100644 index 000000000000..ab5bc4420738 Binary files /dev/null and b/document/public/imgs/wecom-bot-3.png differ diff --git a/document/public/imgs/wecom-bot-4.png b/document/public/imgs/wecom-bot-4.png new file mode 100644 index 000000000000..fb32ff4ea880 Binary files /dev/null and b/document/public/imgs/wecom-bot-4.png differ diff --git a/document/public/imgs/wecom-bot-5.png b/document/public/imgs/wecom-bot-5.png new file mode 100644 index 000000000000..0a61396fea5f Binary files /dev/null and b/document/public/imgs/wecom-bot-5.png differ diff --git a/document/public/imgs/wecom-bot-6.png b/document/public/imgs/wecom-bot-6.png new file mode 100644 index 000000000000..7f5bb961d54b Binary files /dev/null and b/document/public/imgs/wecom-bot-6.png differ diff --git a/document/public/imgs/wecom-bot-7.png b/document/public/imgs/wecom-bot-7.png new file mode 100644 index 000000000000..ba8bab218bd7 Binary files /dev/null and b/document/public/imgs/wecom-bot-7.png differ diff --git a/document/public/imgs/wecom-bot-8.png b/document/public/imgs/wecom-bot-8.png new file mode 100644 index 000000000000..c16439789154 Binary files /dev/null and b/document/public/imgs/wecom-bot-8.png differ diff --git a/document/public/imgs/wecom-bot-9.png b/document/public/imgs/wecom-bot-9.png new file mode 100644 index 000000000000..65f0764b5801 Binary files /dev/null and b/document/public/imgs/wecom-bot-9.png differ diff --git a/package.json b/package.json index c6107570b2a8..ec178620f8cb 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,6 @@ }, "devDependencies": { "@chakra-ui/cli": "^2.4.1", - "typescript": "^5.1.3", "@typescript-eslint/eslint-plugin": "^6.21.0", "@typescript-eslint/parser": "^6.21.0", "@vitest/coverage-v8": "^3.0.9", @@ -28,13 +27,14 @@ "eslint-config-next": "^14.1.0", "husky": "^8.0.3", "i18next": "23.16.8", + "js-yaml": "^4.1.0", "lint-staged": "^13.3.0", + "mongodb-memory-server": "^10.1.4", "next-i18next": "15.4.2", "prettier": "3.2.4", "react-i18next": "14.1.2", + "typescript": "^5.1.3", "vitest": "^3.0.9", - "js-yaml": "^4.1.0", - "mongodb-memory-server": "^10.1.4", "zhlint": "^0.7.4" }, "lint-staged": { diff --git a/packages/global/common/system/types/index.d.ts b/packages/global/common/system/types/index.d.ts index c52feedb443e..fa5bc8a7f68a 100644 --- a/packages/global/common/system/types/index.d.ts +++ b/packages/global/common/system/types/index.d.ts @@ -70,6 +70,7 @@ export type FastGPTFeConfigsType = { show_dataset_yuque?: boolean; show_publish_feishu?: boolean; show_publish_dingtalk?: boolean; + show_publish_wecom?: boolean; show_publish_offiaccount?: boolean; show_dataset_enhance?: boolean; diff --git a/packages/global/core/workflow/runtime/utils.ts b/packages/global/core/workflow/runtime/utils.ts index 575d9fc47404..71279c0adce9 100644 --- a/packages/global/core/workflow/runtime/utils.ts +++ b/packages/global/core/workflow/runtime/utils.ts @@ -109,7 +109,7 @@ export const valueTypeFormat = (value: any, valueType?: WorkflowIOValueTypeEnum) return typeof value === 'object' ? JSON.stringify(value) : String(value); } if (valueType === WorkflowIOValueTypeEnum.number) { - if (value === '') return undefined; + if (value === '') return null; return Number(value); } if (valueType === WorkflowIOValueTypeEnum.boolean) { diff --git a/packages/global/package.json b/packages/global/package.json index 98261ede6748..75e16b2f6e33 100644 --- a/packages/global/package.json +++ b/packages/global/package.json @@ -5,7 +5,7 @@ "@fastgpt-sdk/plugin": "^0.1.16", "@apidevtools/swagger-parser": "^10.1.0", "@bany/curl-to-json": "^1.2.8", - "axios": "^1.8.2", + "axios": "^1.12.1", "cron-parser": "^4.9.0", "dayjs": "^1.11.7", "encoding": "^0.1.13", @@ -13,7 +13,7 @@ "jschardet": "3.1.1", "json5": "^2.2.3", "nanoid": "^5.1.3", - "next": "14.2.28", + "next": "14.2.32", "openai": "4.61.0", "openapi-types": "^12.1.3", "timezones-list": "^3.0.2", diff --git a/packages/global/support/permission/collaborator.d.ts b/packages/global/support/permission/collaborator.d.ts index 150864c5d3f3..6e927c8122e3 100644 --- a/packages/global/support/permission/collaborator.d.ts +++ b/packages/global/support/permission/collaborator.d.ts @@ -1,31 +1,33 @@ +import type { UpdateAppCollaboratorBody } from 'core/app/collaborator'; import type { RequireOnlyOne } from '../../common/type/utils'; import { RequireAtLeastOne } from '../../common/type/utils'; import type { Permission } from './controller'; -import type { PermissionValueType } from './type'; +import type { PermissionValueType, RoleValueType } from './type'; -export type CollaboratorItemType = { - teamId: string; - permission: Permission; - name: string; - avatar: string; -} & RequireOnlyOne<{ +export type CollaboratorIdType = RequireOnlyOne<{ tmbId: string; groupId: string; orgId: string; }>; -export type UpdateClbPermissionProps = { - members?: string[]; - groups?: string[]; - orgs?: string[]; -} & (addOnly extends true - ? {} - : { - permission: PermissionValueType; - }); +export type CollaboratorItemDetailType = { + teamId: string; + permission: Permission; + name: string; + avatar: string; +} & CollaboratorIdType; + +export type CollaboratorItemType = { + permission: PermissionValueType; +} & CollaboratorIdType; -export type DeletePermissionQuery = RequireOnlyOne<{ - tmbId?: string; - groupId?: string; - orgId?: string; -}>; +export type UpdateClbPermissionProps = { + collaborators: CollaboratorItemType[]; +}; + +export type DeletePermissionQuery = CollaboratorIdType; + +export type CollaboratorListType = { + clbs: CollaboratorItemDetailType[]; + parentClbs?: CollaboratorItemDetailType[]; +}; diff --git a/packages/global/support/permission/type.d.ts b/packages/global/support/permission/type.ts similarity index 82% rename from packages/global/support/permission/type.d.ts rename to packages/global/support/permission/type.ts index db0a085e635c..c1752210a0e3 100644 --- a/packages/global/support/permission/type.d.ts +++ b/packages/global/support/permission/type.ts @@ -1,10 +1,8 @@ import type { UserModelSchema } from '../user/type'; import type { RequireOnlyOne } from '../../common/type/utils'; import type { TeamMemberSchema } from '../user/team/type'; -import { MemberGroupSchemaType } from './memberGroup/type'; -import type { TeamMemberWithUserSchema } from '../user/team/type'; -import type { CommonPerKeyEnum, CommonRoleKeyEnum } from './constant'; -import { AuthUserTypeEnum, type CommonPerKeyEnum, type PerResourceTypeEnum } from './constant'; +import type { CommonRoleKeyEnum } from './constant'; +import { type CommonPerKeyEnum, type PerResourceTypeEnum } from './constant'; // 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. @@ -18,14 +16,14 @@ export type ResourceType = `${PerResourceTypeEnum}`; /** * Define the roles. Each role is a binary number, only one bit is set to 1. */ -export type RoleListType = Readonly< +export type RoleListType = Readonly< Record< T | CommonRoleKeyEnum, Readonly<{ name: string; description: string; value: RoleValueType; - checkBoxType: 'single' | 'multiple'; + checkBoxType: 'single' | 'multiple' | 'hidden'; }> > >; @@ -43,7 +41,7 @@ export type RoleListType = Readonly< * write: 0b110, // bad, should be 0b010 * } */ -export type PermissionListType = Readonly< +export type PermissionListType = Readonly< Record >; diff --git a/packages/global/support/permission/user/constant.ts b/packages/global/support/permission/user/constant.ts index 9a99d873bc66..890f50d796d2 100644 --- a/packages/global/support/permission/user/constant.ts +++ b/packages/global/support/permission/user/constant.ts @@ -31,11 +31,13 @@ export const TeamPerList: PermissionListType = { export const TeamRoleList: RoleListType = { [CommonPerKeyEnum.read]: { ...CommonRoleList[CommonPerKeyEnum.read], + name: i18nT('common:permission.common_member'), value: 0b000100 }, [CommonPerKeyEnum.write]: { ...CommonRoleList[CommonPerKeyEnum.write], - value: 0b000010 + value: 0b000010, + checkBoxType: 'hidden' }, [CommonPerKeyEnum.manage]: { ...CommonRoleList[CommonPerKeyEnum.manage], diff --git a/packages/global/support/permission/utils.ts b/packages/global/support/permission/utils.ts index b39900f0630a..c4249487ab6c 100644 --- a/packages/global/support/permission/utils.ts +++ b/packages/global/support/permission/utils.ts @@ -1,52 +1,182 @@ +import type { CollaboratorIdType, CollaboratorItemType } from './collaborator'; +import { ManageRoleVal, OwnerRoleVal } from './constant'; +import type { RoleValueType } from './type'; import { type PermissionValueType } from './type'; -import { NullRoleVal, PermissionTypeEnum } from './constant'; -import type { Permission } from './controller'; - -/* team public source, or owner source in team */ -export function mongoRPermission({ - teamId, - tmbId, - permission -}: { - teamId: string; - tmbId: string; - permission: Permission; -}) { - if (permission.isOwner) { - return { - teamId - }; - } - return { - teamId, - $or: [{ permission: PermissionTypeEnum.public }, { tmbId }] - }; -} -export function mongoOwnerPermission({ teamId, tmbId }: { teamId: string; tmbId: string }) { - return { - teamId, - tmbId - }; -} - -// return permission-related schema to define the schema of resources -export function getPermissionSchema(defaultPermission: PermissionValueType = NullRoleVal) { - return { - defaultPermission: { - type: Number, - default: defaultPermission - }, - inheritPermission: { - type: Boolean, - default: true - } - }; -} - +/** + * Sum the permission value. + * If no permission value is provided, return undefined to fallback to default value. + * @param per permission value (number) + * @returns sum of permission value + */ export const sumPer = (...per: PermissionValueType[]) => { if (per.length === 0) { // prevent sum 0 value, to fallback to default value return undefined; } - return per.reduce((acc, cur) => acc | cur, 0); + const res = per.reduce((acc, cur) => acc | cur, 0); + if (res < 0) { + // overflowed + return OwnerRoleVal; + } + return res; +}; + +/** + * Check if the update cause conflict (need to remove inheritance permission). + * Conflict condition: + * The updated collaborator is a parent collaborator. + * @param parentClbs parent collaborators + * @param oldChildClbs old child collaborators + * @param newChildClbs new child collaborators + */ +export const checkRoleUpdateConflict = ({ + parentClbs, + newChildClbs +}: { + parentClbs: CollaboratorItemType[]; + newChildClbs: CollaboratorItemType[]; +}): boolean => { + if (parentClbs.length === 0) { + return false; + } + + // Use a Map for faster lookup by teamId + const parentClbRoleMap = new Map( + parentClbs.map((clb) => [ + getCollaboratorId(clb), + { + ...clb + } + ]) + ); + + const changedClbs = getChangedCollaborators({ + newRealClbs: newChildClbs, + oldRealClbs: parentClbs + }); + + for (const changedClb of changedClbs) { + const parent = parentClbRoleMap.get(getCollaboratorId(changedClb)); + if (parent && ((changedClb.changedRole & parent.permission) !== 0 || changedClb.deleted)) { + return true; + } + } + + return false; +}; + +export type ChangedClbType = { + changedRole: RoleValueType; + deleted: boolean; +} & CollaboratorIdType; + +/** + * Get changed collaborators. + * return empty array if all collaborators are unchanged. + * + * for each return item: + * ```typescript + * { + * // ... ids + * changedRole: number; // set bit means the role is changed + * deleted: boolean; // is deleted + * } + * ``` + * + * **special**: for low 3 bit: always get the lowest change, unset the higher change. + */ +export const getChangedCollaborators = ({ + oldRealClbs, + newRealClbs +}: { + oldRealClbs: CollaboratorItemType[]; + newRealClbs: CollaboratorItemType[]; +}): ChangedClbType[] => { + if (oldRealClbs.length === 0) { + return newRealClbs.map((clb) => ({ + ...clb, + changedRole: clb.permission, + deleted: false + })); + } + const oldClbsMap = new Map(oldRealClbs.map((clb) => [getCollaboratorId(clb), clb])); + const changedClbs: ChangedClbType[] = []; + for (const newClb of newRealClbs) { + const oldClb = oldClbsMap.get(getCollaboratorId(newClb)); + if (!oldClb) { + changedClbs.push({ + ...newClb, + changedRole: newClb.permission, + deleted: false + }); + continue; + } + const changedRole = oldClb.permission ^ newClb.permission; + if (changedRole) { + changedClbs.push({ + ...newClb, + changedRole, + deleted: false + }); + } + } + + for (const oldClb of oldRealClbs) { + const newClb = newRealClbs.find((clb) => getCollaboratorId(clb) === getCollaboratorId(oldClb)); + if (!newClb) { + changedClbs.push({ + ...oldClb, + changedRole: oldClb.permission, + deleted: true + }); + } + } + + changedClbs.forEach((clb) => { + // For the lowest 3 bits, only keep the lowest set bit as 1, clear other lower bits, keep higher bits unchanged + const low3 = clb.changedRole & 0b111; + const lowestBit = low3 & -low3; + clb.changedRole = (clb.changedRole & ~0b111) | lowestBit; + }); + + return changedClbs; +}; + +export const getCollaboratorId = (clb: CollaboratorIdType) => + (clb.tmbId || clb.groupId || clb.orgId)!; + +export const mergeCollaboratorList = ({ + parentClbs, + childClbs +}: { + parentClbs: T[]; + childClbs: T[]; +}) => { + const idToClb = new Map(); + + // Add all items from list1 + for (const parentClb of parentClbs) { + if (parentClb.permission === OwnerRoleVal) { + idToClb.set(getCollaboratorId(parentClb), { ...parentClb, permission: ManageRoleVal }); + continue; + } + idToClb.set(getCollaboratorId(parentClb), { ...parentClb }); + } + + // Merge permissions from list2 + for (const childClb of childClbs) { + const id = getCollaboratorId(childClb); + if (idToClb.has(id)) { + // If already exists, merge permission bits + const original = idToClb.get(id)!; + idToClb.set(id, { + ...original, + permission: sumPer(original.permission, childClb.permission)! + }); + } else { + idToClb.set(id, { ...childClb }); + } + } + + return Array.from(idToClb.values()); }; diff --git a/packages/global/support/wallet/bill/api.d.ts b/packages/global/support/wallet/bill/api.d.ts index f1b79ae8a0e5..9cec7c44c668 100644 --- a/packages/global/support/wallet/bill/api.d.ts +++ b/packages/global/support/wallet/bill/api.d.ts @@ -1,5 +1,5 @@ import type { StandardSubLevelEnum, SubModeEnum } from '../sub/constants'; -import type { BillTypeEnum } from './constants'; +import type { BillTypeEnum, BillPayWayEnum } from './constants'; import { DrawBillQRItem } from './constants'; export type CreateOrderResponse = { diff --git a/packages/service/common/buffer/rawText/controller.ts b/packages/service/common/buffer/rawText/controller.ts index 306edac32914..d16c9c59e185 100644 --- a/packages/service/common/buffer/rawText/controller.ts +++ b/packages/service/common/buffer/rawText/controller.ts @@ -1,5 +1,5 @@ import { retryFn } from '@fastgpt/global/common/system/utils'; -import { connectionMongo } from '../../mongo'; +import { connectionMongo, Types } from '../../mongo'; import { MongoRawTextBufferSchema, bucketName } from './schema'; import { addLog } from '../../system/log'; import { setCron } from '../../system/cron'; @@ -86,7 +86,7 @@ export const getRawTextBuffer = async (sourceId: string) => { } // Read file content - const downloadStream = gridBucket.openDownloadStream(bufferData._id); + const downloadStream = gridBucket.openDownloadStream(new Types.ObjectId(bufferData._id)); const fileBuffers = await gridFsStream2Buffer(downloadStream); @@ -120,7 +120,7 @@ export const deleteRawTextBuffer = async (sourceId: string): Promise => return false; } - await gridBucket.delete(buffer._id); + await gridBucket.delete(new Types.ObjectId(buffer._id)); return true; }); }; @@ -155,7 +155,7 @@ export const clearExpiredRawTextBufferCron = async () => { for (const item of data) { try { - await gridBucket.delete(item._id); + await gridBucket.delete(new Types.ObjectId(item._id)); } catch (error) { addLog.error('Delete expired raw text buffer error', error); } diff --git a/packages/service/common/mongo/index.ts b/packages/service/common/mongo/index.ts index 0abb1e615f36..5666d6057464 100644 --- a/packages/service/common/mongo/index.ts +++ b/packages/service/common/mongo/index.ts @@ -64,6 +64,33 @@ const addCommonMiddleware = (schema: mongoose.Schema) => { } next(); }); + + // Convert _id to string + schema.post(/^find/, function (docs) { + if (!docs) return; + + const convertObjectIds = (obj: any) => { + if (!obj) return; + + // Convert _id + if (obj._id && obj._id.toString) { + obj._id = obj._id.toString(); + } + + // Convert other ObjectId fields + Object.keys(obj).forEach((key) => { + if (obj[key] && obj[key]._bsontype === 'ObjectId') { + obj[key] = obj[key].toString(); + } + }); + }; + + if (Array.isArray(docs)) { + docs.forEach((doc) => convertObjectIds(doc)); + } else { + convertObjectIds(docs); + } + }); }); return schema; diff --git a/packages/service/common/mongo/utils.ts b/packages/service/common/mongo/utils.ts index 294bfdd47e90..ac249cafaaad 100644 --- a/packages/service/common/mongo/utils.ts +++ b/packages/service/common/mongo/utils.ts @@ -4,3 +4,10 @@ export const readFromSecondary = { readPreference: ReadPreference.SECONDARY_PREFERRED, // primary | primaryPreferred | secondary | secondaryPreferred | nearest readConcern: 'local' as any // local | majority | linearizable | available }; + +export const writePrimary = { + writeConcern: { + w: 1, + journal: false + } +}; diff --git a/packages/service/common/redis/cache.ts b/packages/service/common/redis/cache.ts index 95fe68c866e0..bcd496a6c692 100644 --- a/packages/service/common/redis/cache.ts +++ b/packages/service/common/redis/cache.ts @@ -56,3 +56,20 @@ export const delRedisCache = async (key: string) => { const redis = getGlobalRedisConnection(); await retryFn(() => redis.del(getCacheKey(key))); }; + +export const appendRedisCache = async ( + key: string, + value: string | Buffer | number, + expireSeconds?: number +) => { + try { + const redis = getGlobalRedisConnection(); + await retryFn(() => redis.append(getCacheKey(key), value)); + if (expireSeconds) { + await redis.expire(getCacheKey(key), expireSeconds); + } + } catch (error) { + addLog.error('Append cache error:', error); + return Promise.reject(error); + } +}; diff --git a/packages/service/common/response/index.ts b/packages/service/common/response/index.ts index 5d2f58db6ea8..350a028472eb 100644 --- a/packages/service/common/response/index.ts +++ b/packages/service/common/response/index.ts @@ -2,9 +2,9 @@ import type { NextApiResponse } from 'next'; import { SseResponseEventEnum } from '@fastgpt/global/core/workflow/runtime/constants'; import { proxyError, ERROR_RESPONSE, ERROR_ENUM } from '@fastgpt/global/common/error/errorCode'; import { addLog } from '../system/log'; -import { clearCookie } from '../../support/permission/controller'; import { replaceSensitiveText } from '@fastgpt/global/common/string/tools'; import { UserError } from '@fastgpt/global/common/error/utils'; +import { clearCookie } from '../../support/permission/auth/common'; export interface ResponseType { code: number; diff --git a/packages/service/core/ai/llm/request.ts b/packages/service/core/ai/llm/request.ts index 185661657be5..41b84ced2f86 100644 --- a/packages/service/core/ai/llm/request.ts +++ b/packages/service/core/ai/llm/request.ts @@ -645,4 +645,4 @@ const createChatCompletion = async ({ } return Promise.reject(error); } -}; +}; \ No newline at end of file diff --git a/packages/service/core/app/controller.ts b/packages/service/core/app/controller.ts index 303b20e28d94..5cf05f1fb405 100644 --- a/packages/service/core/app/controller.ts +++ b/packages/service/core/app/controller.ts @@ -157,23 +157,18 @@ export const onDelOneApp = async ({ ).lean(); await Promise.all(evalJobs.map((evalJob) => removeEvaluationJob(evalJob._id))); + // Delete chats + await deleteChatFiles({ appId }); + await MongoChatItem.deleteMany({ + appId + }); + await MongoChat.deleteMany({ + appId + }); + const del = async (session: ClientSession) => { for await (const app of apps) { const appId = app._id; - // Chats - await deleteChatFiles({ appId }); - await MongoChatItem.deleteMany( - { - appId - }, - { session } - ); - await MongoChat.deleteMany( - { - appId - }, - { session } - ); // 删除分享链接 await MongoOutLink.deleteMany({ @@ -205,6 +200,7 @@ export const onDelOneApp = async ({ { $pull: { quickAppIds: { id: String(appId) } } } ).session(session); + // Del permission await MongoResourcePermission.deleteMany({ resourceType: PerResourceTypeEnum.app, teamId, diff --git a/packages/service/core/chat/saveChat.ts b/packages/service/core/chat/saveChat.ts index 632c84e1cd35..09e466ade7b2 100644 --- a/packages/service/core/chat/saveChat.ts +++ b/packages/service/core/chat/saveChat.ts @@ -15,6 +15,7 @@ import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant'; import { DispatchNodeResponseKeyEnum } from '@fastgpt/global/core/workflow/runtime/constants'; import { extractDeepestInteractive } from '@fastgpt/global/core/workflow/runtime/utils'; import { MongoAppChatLog } from '../app/logs/chatLogsSchema'; +import { writePrimary } from '../../common/mongo/utils'; type Props = { chatId: string; @@ -115,7 +116,7 @@ export async function saveChat({ }); await mongoSessionRun(async (session) => { - const [{ _id: chatItemIdHuman }, { _id: chatItemIdAi }] = await MongoChatItem.insertMany( + const [{ _id: chatItemIdHuman }, { _id: chatItemIdAi }] = await MongoChatItem.create( processedContent.map((item) => ({ chatId, teamId, @@ -123,7 +124,7 @@ export async function saveChat({ appId, ...item })), - { session } + { session, ordered: true, ...writePrimary } ); await MongoChat.updateOne( @@ -152,7 +153,8 @@ export async function saveChat({ }, { session, - upsert: true + upsert: true, + ...writePrimary } ); @@ -215,7 +217,8 @@ export async function saveChat({ } }, { - upsert: true + upsert: true, + ...writePrimary } ); } catch (error) { @@ -223,9 +226,15 @@ export async function saveChat({ } if (isUpdateUseTime) { - await MongoApp.findByIdAndUpdate(appId, { - updateTime: new Date() - }).catch(); + await MongoApp.updateOne( + { _id: appId }, + { + updateTime: new Date() + }, + { + ...writePrimary + } + ).catch(); } } catch (error) { addLog.error(`update chat history error`, error); diff --git a/packages/service/core/dataset/image/controller.ts b/packages/service/core/dataset/image/controller.ts index ff3628b6f121..ca597eae44b5 100644 --- a/packages/service/core/dataset/image/controller.ts +++ b/packages/service/core/dataset/image/controller.ts @@ -142,7 +142,7 @@ export const clearExpiredDatasetImageCron = async () => { for (const item of data) { try { - await gridBucket.delete(item._id); + await gridBucket.delete(new Types.ObjectId(item._id)); } catch (error) { addLog.error('Delete expired dataset image error', error); } diff --git a/packages/service/package.json b/packages/service/package.json index f4523040c46f..bb146d44b61a 100644 --- a/packages/service/package.json +++ b/packages/service/package.json @@ -15,7 +15,7 @@ "@vercel/otel": "^1.13.0", "@xmldom/xmldom": "^0.8.10", "@zilliz/milvus2-sdk-node": "2.4.10", - "axios": "^1.8.2", + "axios": "^1.12.1", "bullmq": "^5.52.2", "chalk": "^5.3.0", "cheerio": "1.0.0-rc.12", @@ -38,7 +38,7 @@ "mongoose": "^8.10.1", "multer": "2.0.2", "mysql2": "^3.11.3", - "next": "14.2.28", + "next": "14.2.32", "nextjs-cors": "^2.2.0", "node-cron": "^3.0.3", "node-xlsx": "^0.24.0", diff --git a/packages/service/support/permission/app/auth.ts b/packages/service/support/permission/app/auth.ts index 0accefdc7d7d..0f1e42466c31 100644 --- a/packages/service/support/permission/app/auth.ts +++ b/packages/service/support/permission/app/auth.ts @@ -1,15 +1,15 @@ /* Auth app permission */ import { MongoApp } from '../../../core/app/schema'; import { type AppDetailType } from '@fastgpt/global/core/app/type.d'; -import { parseHeaderCert } from '../controller'; import { + NullRoleVal, PerResourceTypeEnum, ReadPermissionVal, ReadRoleVal } from '@fastgpt/global/support/permission/constant'; import { AppErrEnum } from '@fastgpt/global/common/error/code/app'; import { getTmbInfoByTmbId } from '../../user/team/controller'; -import { getResourcePermission } from '../controller'; +import { getTmbPermission } from '../controller'; import { AppPermission } from '@fastgpt/global/support/permission/app/controller'; import { type PermissionValueType } from '@fastgpt/global/support/permission/type'; import { AppFolderTypeList, AppTypeEnum } from '@fastgpt/global/core/app/constants'; @@ -18,6 +18,8 @@ import { PluginSourceEnum } from '@fastgpt/global/core/app/plugin/constants'; import { type AuthModeType, type AuthResponseType } from '../type'; import { splitCombinePluginId } from '@fastgpt/global/core/app/plugin/utils'; import { AppReadChatLogPerVal } from '@fastgpt/global/support/permission/app/constant'; +import { parseHeaderCert } from '../auth/common'; +import { sumPer } from '@fastgpt/global/support/permission/utils'; export const authPluginByTmbId = async ({ tmbId, @@ -90,53 +92,27 @@ export const authAppByTmbId = async ({ const isOwner = tmbPer.isOwner || String(app.tmbId) === String(tmbId); - const { Per } = await (async () => { - if (isOwner) { - return { - Per: new AppPermission({ isOwner: true }) - }; - } - - if ( - AppFolderTypeList.includes(app.type) || - app.inheritPermission === false || - !app.parentId - ) { - // 1. is a folder. (Folders have completely permission) - // 2. inheritPermission is false. - // 3. is root folder/app. - const role = await getResourcePermission({ - teamId, - tmbId, - resourceId: appId, - resourceType: PerResourceTypeEnum.app - }); - const Per = new AppPermission({ role, isOwner }); - - if (app.favourite || app.quick) { - Per.addRole(ReadRoleVal); - } - - return { - Per - }; - } else { - // is not folder and inheritPermission is true and is not root folder. - const { app: parent } = await authAppByTmbId({ - tmbId, - appId: app.parentId, - per - }); - - const Per = new AppPermission({ - role: parent.permission.role, - isOwner - }); - return { - Per - }; - } - })(); + const isGetParentClb = + app.inheritPermission && !AppFolderTypeList.includes(app.type) && !!app.parentId; + + const [folderPer = NullRoleVal, myPer = NullRoleVal] = await Promise.all([ + isGetParentClb + ? getTmbPermission({ + teamId, + tmbId, + resourceId: app.parentId!, + resourceType: PerResourceTypeEnum.app + }) + : NullRoleVal, + getTmbPermission({ + teamId, + tmbId, + resourceId: appId, + resourceType: PerResourceTypeEnum.app + }) + ]); + + const Per = new AppPermission({ role: sumPer(folderPer, myPer), isOwner }); if (!Per.checkPer(per)) { return Promise.reject(AppErrEnum.unAuthApp); diff --git a/packages/service/support/permission/auth/common.ts b/packages/service/support/permission/auth/common.ts index 31b0b13cdb20..23f40362a406 100644 --- a/packages/service/support/permission/auth/common.ts +++ b/packages/service/support/permission/auth/common.ts @@ -1,7 +1,13 @@ -import { parseHeaderCert } from '../controller'; +import type { ReqHeaderAuthType } from '../type'; import { type AuthModeType } from '../type'; import { SERVICE_LOCAL_HOST } from '../../../common/system/tools'; import { type ApiRequestProps } from '../../../type/next'; +import type { NextApiResponse } from 'next'; +import Cookie from 'cookie'; +import { ERROR_ENUM } from '@fastgpt/global/common/error/errorCode'; +import { authUserSession } from '../../../support/user/session'; +import { authOpenApiKey } from '../../../support/openapi/auth'; +import { AuthUserTypeEnum } from '@fastgpt/global/support/permission/constant'; export const authCert = async (props: AuthModeType) => { const result = await parseHeaderCert(props); @@ -19,3 +25,149 @@ export const authRequestFromLocal = ({ req }: { req: ApiRequestProps }) => { return Promise.reject('Invalid request'); } }; + +export async function parseHeaderCert({ + req, + authToken = false, + authRoot = false, + authApiKey = false +}: AuthModeType) { + // parse jwt + async function authCookieToken(cookie?: string, token?: string) { + // 获取 cookie + const cookies = Cookie.parse(cookie || ''); + const cookieToken = token || cookies[TokenName]; + + if (!cookieToken) { + return Promise.reject(ERROR_ENUM.unAuthorization); + } + + return { ...(await authUserSession(cookieToken)), sessionId: cookieToken }; + } + // from authorization get apikey + async function parseAuthorization(authorization?: string) { + if (!authorization) { + return Promise.reject(ERROR_ENUM.unAuthorization); + } + + // Bearer fastgpt-xxxx-appId + const auth = authorization.split(' ')[1]; + if (!auth) { + return Promise.reject(ERROR_ENUM.unAuthorization); + } + + const { apikey, appId: authorizationAppid = '' } = await (async () => { + const arr = auth.split('-'); + // abandon + if (arr.length === 3) { + return { + apikey: `${arr[0]}-${arr[1]}`, + appId: arr[2] + }; + } + if (arr.length === 2) { + return { + apikey: auth + }; + } + return Promise.reject(ERROR_ENUM.unAuthorization); + })(); + + // auth apikey + const { teamId, tmbId, appId: apiKeyAppId = '', sourceName } = await authOpenApiKey({ apikey }); + + return { + uid: '', + teamId, + tmbId, + apikey, + appId: apiKeyAppId || authorizationAppid, + sourceName + }; + } + // root user + async function parseRootKey(rootKey?: string) { + if (!rootKey || !process.env.ROOT_KEY || rootKey !== process.env.ROOT_KEY) { + return Promise.reject(ERROR_ENUM.unAuthorization); + } + } + + const { cookie, token, rootkey, authorization } = (req.headers || {}) as ReqHeaderAuthType; + + const { uid, teamId, tmbId, appId, openApiKey, authType, isRoot, sourceName, sessionId } = + await (async () => { + if (authApiKey && authorization) { + // apikey from authorization + const authResponse = await parseAuthorization(authorization); + return { + uid: authResponse.uid, + teamId: authResponse.teamId, + tmbId: authResponse.tmbId, + appId: authResponse.appId, + openApiKey: authResponse.apikey, + authType: AuthUserTypeEnum.apikey, + sourceName: authResponse.sourceName + }; + } + if (authToken && (token || cookie)) { + // user token(from fastgpt web) + const res = await authCookieToken(cookie, token); + + return { + uid: res.userId, + teamId: res.teamId, + tmbId: res.tmbId, + appId: '', + openApiKey: '', + authType: AuthUserTypeEnum.token, + isRoot: res.isRoot, + sessionId: res.sessionId + }; + } + if (authRoot && rootkey) { + await parseRootKey(rootkey); + // root user + return { + uid: '', + teamId: '', + tmbId: '', + appId: '', + openApiKey: '', + authType: AuthUserTypeEnum.root, + isRoot: true + }; + } + + return Promise.reject(ERROR_ENUM.unAuthorization); + })(); + + if (!authRoot && (!teamId || !tmbId)) { + return Promise.reject(ERROR_ENUM.unAuthorization); + } + + return { + userId: String(uid), + teamId: String(teamId), + tmbId: String(tmbId), + appId, + authType, + sourceName, + apikey: openApiKey, + isRoot: !!isRoot, + sessionId + }; +} + +/* set cookie */ +export const TokenName = 'fastgpt_token'; +export const setCookie = (res: NextApiResponse, token: string) => { + res.setHeader( + 'Set-Cookie', + `${TokenName}=${token}; Path=/; HttpOnly; Max-Age=604800; Samesite=Strict;` + ); +}; + +/* clear cookie */ +export const clearCookie = (res: NextApiResponse) => { + res.setHeader('Set-Cookie', `${TokenName}=; Path=/; Max-Age=0`); +}; diff --git a/packages/service/support/permission/auth/file.ts b/packages/service/support/permission/auth/file.ts index 6ea324d04466..bc4d868072a4 100644 --- a/packages/service/support/permission/auth/file.ts +++ b/packages/service/support/permission/auth/file.ts @@ -1,11 +1,15 @@ import { type AuthModeType, type AuthResponseType } from '../type'; import { type DatasetFileSchema } from '@fastgpt/global/core/dataset/type'; -import { parseHeaderCert } from '../controller'; import { getFileById } from '../../../common/file/gridfs/controller'; -import { BucketNameEnum } from '@fastgpt/global/common/file/constants'; +import { BucketNameEnum, bucketNameMap } from '@fastgpt/global/common/file/constants'; import { CommonErrEnum } from '@fastgpt/global/common/error/code/common'; import { OwnerPermissionVal, ReadRoleVal } from '@fastgpt/global/support/permission/constant'; import { Permission } from '@fastgpt/global/support/permission/controller'; +import type { FileTokenQuery } from '@fastgpt/global/common/file/type'; +import { addMinutes } from 'date-fns'; +import { parseHeaderCert } from './common'; +import jwt from 'jsonwebtoken'; +import { ERROR_ENUM } from '@fastgpt/global/common/error/errorCode'; export const authCollectionFile = async ({ fileId, @@ -46,3 +50,45 @@ export const authCollectionFile = async ({ file }; }; + +/* file permission */ +export const createFileToken = (data: FileTokenQuery) => { + if (!process.env.FILE_TOKEN_KEY) { + return Promise.reject('System unset FILE_TOKEN_KEY'); + } + + const expireMinutes = + data.customExpireMinutes ?? bucketNameMap[data.bucketName].previewExpireMinutes; + const expiredTime = Math.floor(addMinutes(new Date(), expireMinutes).getTime() / 1000); + + const key = (process.env.FILE_TOKEN_KEY as string) ?? 'filetoken'; + const token = jwt.sign( + { + ...data, + exp: expiredTime + }, + key + ); + return Promise.resolve(token); +}; + +export const authFileToken = (token?: string) => + new Promise((resolve, reject) => { + if (!token) { + return reject(ERROR_ENUM.unAuthFile); + } + const key = (process.env.FILE_TOKEN_KEY as string) ?? 'filetoken'; + + jwt.verify(token, key, (err, decoded: any) => { + if (err || !decoded.bucketName || !decoded?.teamId || !decoded?.fileId) { + reject(ERROR_ENUM.unAuthFile); + return; + } + resolve({ + bucketName: decoded.bucketName, + teamId: decoded.teamId, + uid: decoded.uid, + fileId: decoded.fileId + }); + }); + }); diff --git a/packages/service/support/permission/auth/openapi.ts b/packages/service/support/permission/auth/openapi.ts index 206eff13d8b7..a2a404fce4a5 100644 --- a/packages/service/support/permission/auth/openapi.ts +++ b/packages/service/support/permission/auth/openapi.ts @@ -1,11 +1,11 @@ import { type AuthModeType, type AuthResponseType } from '../type'; import { type OpenApiSchema } from '@fastgpt/global/support/openapi/type'; -import { parseHeaderCert } from '../controller'; import { getTmbInfoByTmbId } from '../../user/team/controller'; import { MongoOpenApi } from '../../openapi/schema'; import { OpenApiErrEnum } from '@fastgpt/global/common/error/code/openapi'; import { OwnerPermissionVal } from '@fastgpt/global/support/permission/constant'; import { authAppByTmbId } from '../app/auth'; +import { parseHeaderCert } from './common'; export async function authOpenApiKeyCrud({ id, diff --git a/packages/service/support/permission/controller.ts b/packages/service/support/permission/controller.ts index 96279c472630..fc08b751f12d 100644 --- a/packages/service/support/permission/controller.ts +++ b/packages/service/support/permission/controller.ts @@ -1,27 +1,24 @@ -import Cookie from 'cookie'; -import { ERROR_ENUM } from '@fastgpt/global/common/error/errorCode'; -import jwt from 'jsonwebtoken'; -import { type NextApiResponse, type NextApiRequest } from 'next'; -import type { AuthModeType, ReqHeaderAuthType } from './type.d'; +import type { ClientSession, AnyBulkWriteOperation } from '../../common/mongo'; import type { PerResourceTypeEnum } from '@fastgpt/global/support/permission/constant'; -import { AuthUserTypeEnum } from '@fastgpt/global/support/permission/constant'; -import { authOpenApiKey } from '../openapi/auth'; -import { type FileTokenQuery } from '@fastgpt/global/common/file/type'; +import { ManageRoleVal, OwnerRoleVal } from '@fastgpt/global/support/permission/constant'; import { MongoResourcePermission } from './schema'; -import { type ClientSession } from 'mongoose'; +import type { ResourcePermissionType, ResourceType } from '@fastgpt/global/support/permission/type'; import { type PermissionValueType } from '@fastgpt/global/support/permission/type'; -import { bucketNameMap } from '@fastgpt/global/common/file/constants'; -import { addMinutes } from 'date-fns'; import { getGroupsByTmbId } from './memberGroup/controllers'; import { Permission } from '@fastgpt/global/support/permission/controller'; import { type ParentIdType } from '@fastgpt/global/common/parentFolder/type'; -import { CommonErrEnum } from '@fastgpt/global/common/error/code/common'; -import { type MemberGroupSchemaType } from '@fastgpt/global/support/permission/memberGroup/type'; -import { type TeamMemberSchema } from '@fastgpt/global/support/user/team/type'; -import { type OrgSchemaType } from '@fastgpt/global/support/user/team/org/type'; import { getOrgIdSetWithParentByTmbId } from './org/controllers'; -import { authUserSession } from '../user/session'; -import { sumPer } from '@fastgpt/global/support/permission/utils'; +import { getCollaboratorId, sumPer } from '@fastgpt/global/support/permission/utils'; +import { type SyncChildrenPermissionResourceType } from './inheritPermission'; +import { pickCollaboratorIdFields } from './utils'; +import type { + CollaboratorItemDetailType, + CollaboratorItemType +} from '@fastgpt/global/support/permission/collaborator'; +import { MongoTeamMember } from '../../support/user/team/teamMemberSchema'; +import { MongoOrgModel } from './org/orgSchema'; +import { MongoMemberGroupModel } from './memberGroup/memberGroupSchema'; +import { DEFAULT_ORG_AVATAR, DEFAULT_TEAM_AVATAR } from '@fastgpt/global/common/system/constants'; /** get resource permission for a team member * If there is no permission for the team member, it will return undefined @@ -31,7 +28,7 @@ import { sumPer } from '@fastgpt/global/support/permission/utils'; * @param resourceId * @returns PermissionValueType | undefined */ -export const getResourcePermission = async ({ +export const getTmbPermission = async ({ resourceType, teamId, tmbId, @@ -106,17 +103,27 @@ export const getResourcePermission = async ({ return sumPer(...groupPers, ...orgPers); }; -export async function getResourceClbsAndGroups({ - resourceId, +/** + * Only get resource's owned clbs, not including parents'. + */ +export async function getResourceOwnedClbs({ resourceType, teamId, + resourceId, session }: { - resourceId: ParentIdType; - resourceType: Omit<`${PerResourceTypeEnum}`, 'team'>; teamId: string; - session: ClientSession; -}) { + session?: ClientSession; +} & ( + | { + resourceType: 'team'; + resourceId?: undefined; + } + | { + resourceType: Omit; + resourceId: ParentIdType; + } +)) { return MongoResourcePermission.find( { resourceId, @@ -124,282 +131,110 @@ export async function getResourceClbsAndGroups({ teamId }, undefined, - { session } + { ...(session ? { session } : {}) } ).lean(); } -export const getClbsAndGroupsWithInfo = async ({ - resourceId, - resourceType, - teamId -}: { - teamId: string; -} & ( - | { - resourceId: ParentIdType; - resourceType: Omit<`${PerResourceTypeEnum}`, 'team'>; - } - | { - resourceType: 'team'; - resourceId?: undefined; - } -)) => - Promise.all([ - MongoResourcePermission.find({ - teamId, - resourceId, - resourceType, - tmbId: { - $exists: true - } - }) - .populate<{ tmb: TeamMemberSchema }>({ - path: 'tmb', - select: 'name userId avatar' - }) - .lean(), - MongoResourcePermission.find({ - teamId, - resourceId, - resourceType, - groupId: { - $exists: true - } - }) - .populate<{ group: MemberGroupSchemaType }>('group', 'name avatar') - .lean(), - MongoResourcePermission.find({ - teamId, - resourceId, - resourceType, - orgId: { - $exists: true - } - }) - .populate<{ org: OrgSchemaType }>({ path: 'org', select: 'name avatar' }) - .lean() - ]); - -export const delResourcePermissionById = (id: string) => { - return MongoResourcePermission.findByIdAndDelete(id); -}; -export const delResourcePermission = ({ - session, - tmbId, - groupId, - orgId, - ...props +export const getClbsInfo = async ({ + clbs, + teamId, + ownerTmbId }: { - resourceType: PerResourceTypeEnum; + clbs: CollaboratorItemType[]; teamId: string; - resourceId: string; - session?: ClientSession; - tmbId?: string; - groupId?: string; - orgId?: string; -}) => { - // either tmbId or groupId or orgId must be provided - if (!tmbId && !groupId && !orgId) { - return Promise.reject(CommonErrEnum.missingParams); - } - - return MongoResourcePermission.deleteOne( - { - ...(tmbId ? { tmbId } : {}), - ...(groupId ? { groupId } : {}), - ...(orgId ? { orgId } : {}), - ...props - }, - { session } - ); -}; - -/* 下面代码等迁移 */ - -export async function parseHeaderCert({ - req, - authToken = false, - authRoot = false, - authApiKey = false -}: AuthModeType) { - // parse jwt - async function authCookieToken(cookie?: string, token?: string) { - // 获取 cookie - const cookies = Cookie.parse(cookie || ''); - const cookieToken = token || cookies[TokenName]; - - if (!cookieToken) { - return Promise.reject(ERROR_ENUM.unAuthorization); - } - - return { ...(await authUserSession(cookieToken)), sessionId: cookieToken }; + ownerTmbId?: string; +}): Promise => { + const tmbIds = []; + const orgIds = []; + const groupIds = []; + + for (const clb of clbs) { + if (clb.tmbId) tmbIds.push(clb.tmbId); + if (clb.orgId) orgIds.push(clb.orgId); + if (clb.groupId) groupIds.push(clb.groupId); } - // from authorization get apikey - async function parseAuthorization(authorization?: string) { - if (!authorization) { - return Promise.reject(ERROR_ENUM.unAuthorization); - } - - // Bearer fastgpt-xxxx-appId - const auth = authorization.split(' ')[1]; - if (!auth) { - return Promise.reject(ERROR_ENUM.unAuthorization); - } - const { apikey, appId: authorizationAppid = '' } = await (async () => { - const arr = auth.split('-'); - // abandon - if (arr.length === 3) { - return { - apikey: `${arr[0]}-${arr[1]}`, - appId: arr[2] - }; - } - if (arr.length === 2) { - return { - apikey: auth - }; - } - return Promise.reject(ERROR_ENUM.unAuthorization); - })(); + const infos = ( + await Promise.all([ + MongoTeamMember.find({ _id: { $in: tmbIds }, teamId }, '_id name avatar').lean(), + MongoOrgModel.find({ _id: { $in: orgIds }, teamId }, '_id name avatar').lean(), + MongoMemberGroupModel.find({ _id: { $in: groupIds }, teamId }, '_id name avatar').lean() + ]) + ).flat(); - // auth apikey - const { teamId, tmbId, appId: apiKeyAppId = '', sourceName } = await authOpenApiKey({ apikey }); + return clbs.map((clb) => { + const info = infos.find((info) => info._id === getCollaboratorId(clb)); return { - uid: '', + ...clb, teamId, - tmbId, - apikey, - appId: apiKeyAppId || authorizationAppid, - sourceName + permission: new Permission({ + role: clb.permission, + isOwner: Boolean(ownerTmbId && clb.tmbId && ownerTmbId === clb.tmbId) + }), + name: info?.name ?? 'Unknown name', + avatar: info?.avatar || (clb.orgId ? DEFAULT_ORG_AVATAR : DEFAULT_TEAM_AVATAR) }; - } - // root user - async function parseRootKey(rootKey?: string) { - if (!rootKey || !process.env.ROOT_KEY || rootKey !== process.env.ROOT_KEY) { - return Promise.reject(ERROR_ENUM.unAuthorization); - } - } - - const { cookie, token, rootkey, authorization } = (req.headers || {}) as ReqHeaderAuthType; - - const { uid, teamId, tmbId, appId, openApiKey, authType, isRoot, sourceName, sessionId } = - await (async () => { - if (authApiKey && authorization) { - // apikey from authorization - const authResponse = await parseAuthorization(authorization); - return { - uid: authResponse.uid, - teamId: authResponse.teamId, - tmbId: authResponse.tmbId, - appId: authResponse.appId, - openApiKey: authResponse.apikey, - authType: AuthUserTypeEnum.apikey, - sourceName: authResponse.sourceName - }; - } - if (authToken && (token || cookie)) { - // user token(from fastgpt web) - const res = await authCookieToken(cookie, token); - - return { - uid: res.userId, - teamId: res.teamId, - tmbId: res.tmbId, - appId: '', - openApiKey: '', - authType: AuthUserTypeEnum.token, - isRoot: res.isRoot, - sessionId: res.sessionId - }; - } - if (authRoot && rootkey) { - await parseRootKey(rootkey); - // root user - return { - uid: '', - teamId: '', - tmbId: '', - appId: '', - openApiKey: '', - authType: AuthUserTypeEnum.root, - isRoot: true - }; - } - - return Promise.reject(ERROR_ENUM.unAuthorization); - })(); - - if (!authRoot && (!teamId || !tmbId)) { - return Promise.reject(ERROR_ENUM.unAuthorization); - } - - return { - userId: String(uid), - teamId: String(teamId), - tmbId: String(tmbId), - appId, - authType, - sourceName, - apikey: openApiKey, - isRoot: !!isRoot, - sessionId - }; -} - -/* set cookie */ -export const TokenName = 'fastgpt_token'; -export const setCookie = (res: NextApiResponse, token: string) => { - res.setHeader( - 'Set-Cookie', - `${TokenName}=${token}; Path=/; HttpOnly; Max-Age=604800; Samesite=Strict;` - ); -}; - -/* clear cookie */ -export const clearCookie = (res: NextApiResponse) => { - res.setHeader('Set-Cookie', `${TokenName}=; Path=/; Max-Age=0`); + }); }; -/* file permission */ -export const createFileToken = (data: FileTokenQuery) => { - if (!process.env.FILE_TOKEN_KEY) { - return Promise.reject('System unset FILE_TOKEN_KEY'); - } - - const expireMinutes = - data.customExpireMinutes ?? bucketNameMap[data.bucketName].previewExpireMinutes; - const expiredTime = Math.floor(addMinutes(new Date(), expireMinutes).getTime() / 1000); +export const createResourceDefaultCollaborators = async ({ + resource, + resourceType, + session, + tmbId +}: { + resource: SyncChildrenPermissionResourceType; + resourceType: PerResourceTypeEnum; - const key = (process.env.FILE_TOKEN_KEY as string) ?? 'filetoken'; - const token = jwt.sign( + // should be provided when inheritPermission is true + session: ClientSession; + tmbId: string; +}) => { + const parentClbs = await getResourceOwnedClbs({ + resourceId: resource.parentId, + resourceType, + teamId: resource.teamId, + session + }); + // 1. add owner into the permission list with owner per + // 2. remove parent's owner permission, instead of manager + + const collaborators: CollaboratorItemType[] = [ + ...parentClbs + .filter((item) => item.tmbId !== tmbId) + .map((clb) => { + if (clb.permission === OwnerRoleVal) { + clb.permission = ManageRoleVal; + } + return clb; + }), { - ...data, - exp: expiredTime - }, - key - ); - return Promise.resolve(token); -}; - -export const authFileToken = (token?: string) => - new Promise((resolve, reject) => { - if (!token) { - return reject(ERROR_ENUM.unAuthFile); + tmbId, + permission: OwnerRoleVal } - const key = (process.env.FILE_TOKEN_KEY as string) ?? 'filetoken'; - - jwt.verify(token, key, (err, decoded: any) => { - if (err || !decoded.bucketName || !decoded?.teamId || !decoded?.fileId) { - reject(ERROR_ENUM.unAuthFile); - return; + ]; + + const ops: AnyBulkWriteOperation[] = []; + + for (const clb of collaborators) { + ops.push({ + updateOne: { + filter: { + ...pickCollaboratorIdFields(clb), + teamId: resource.teamId, + resourceId: resource._id, + resourceType + }, + update: { + $set: { + permission: clb.permission + } + }, + upsert: true } - resolve({ - bucketName: decoded.bucketName, - teamId: decoded.teamId, - uid: decoded.uid, - fileId: decoded.fileId - }); }); - }); + } + + await MongoResourcePermission.bulkWrite(ops, { session }); +}; diff --git a/packages/service/support/permission/dataset/auth.ts b/packages/service/support/permission/dataset/auth.ts index 3b4ca9d8358f..faa53dd13683 100644 --- a/packages/service/support/permission/dataset/auth.ts +++ b/packages/service/support/permission/dataset/auth.ts @@ -1,5 +1,5 @@ import { type PermissionValueType } from '@fastgpt/global/support/permission/type'; -import { getResourcePermission, parseHeaderCert } from '../controller'; +import { getTmbPermission } from '../controller'; import { type CollectionWithDatasetType, type DatasetDataItemType, @@ -9,6 +9,7 @@ import { getTmbInfoByTmbId } from '../../user/team/controller'; import { MongoDataset } from '../../../core/dataset/schema'; import { NullPermissionVal, + NullRoleVal, PerResourceTypeEnum } from '@fastgpt/global/support/permission/constant'; import { DatasetErrEnum } from '@fastgpt/global/common/error/code/dataset'; @@ -21,6 +22,8 @@ import { type ParentIdType } from '@fastgpt/global/common/parentFolder/type'; import { DataSetDefaultRoleVal } from '@fastgpt/global/support/permission/dataset/constant'; import { getDatasetImagePreviewUrl } from '../../../core/dataset/image/utils'; import { i18nT } from '../../../../web/i18n/utils'; +import { parseHeaderCert } from '../auth/common'; +import { sumPer } from '@fastgpt/global/support/permission/utils'; export const authDatasetByTmbId = async ({ tmbId, @@ -61,54 +64,27 @@ export const authDatasetByTmbId = async ({ } const isOwner = tmbPer.isOwner || String(dataset.tmbId) === String(tmbId); + const isGetParentClb = + dataset.inheritPermission && dataset.type !== DatasetTypeEnum.folder && !!dataset.parentId; - // get dataset permission or inherit permission from parent folder. - const { Per } = await (async () => { - if (isOwner) { - return { - Per: new DatasetPermission({ isOwner: true }) - }; - } - if ( - dataset.type === DatasetTypeEnum.folder || - dataset.inheritPermission === false || - !dataset.parentId - ) { - // 1. is a folder. (Folders have completely permission) - // 2. inheritPermission is false. - // 3. is root folder/dataset. - const rp = await getResourcePermission({ - teamId, - tmbId, - resourceId: datasetId, - resourceType: PerResourceTypeEnum.dataset - }); - const Per = new DatasetPermission({ - role: rp, - isOwner - }); - return { - Per - }; - } else { - // is not folder and inheritPermission is true and is not root folder. - const { dataset: parent } = await authDatasetByTmbId({ - tmbId, - datasetId: dataset.parentId, - per, - isRoot - }); - - const Per = new DatasetPermission({ - role: parent.permission.role, - isOwner - }); + const [folderPer = NullRoleVal, myPer = NullRoleVal] = await Promise.all([ + isGetParentClb + ? getTmbPermission({ + teamId, + tmbId, + resourceId: dataset.parentId!, + resourceType: PerResourceTypeEnum.dataset + }) + : NullRoleVal, + getTmbPermission({ + teamId, + tmbId, + resourceId: datasetId, + resourceType: PerResourceTypeEnum.dataset + }) + ]); - return { - Per - }; - } - })(); + const Per = new DatasetPermission({ role: sumPer(folderPer, myPer), isOwner }); if (!Per.checkPer(per)) { return Promise.reject(DatasetErrEnum.unAuthDataset); diff --git a/packages/service/support/permission/evaluation/auth.ts b/packages/service/support/permission/evaluation/auth.ts index 1622ef31e7e8..c5d4266a3d33 100644 --- a/packages/service/support/permission/evaluation/auth.ts +++ b/packages/service/support/permission/evaluation/auth.ts @@ -1,4 +1,3 @@ -import { parseHeaderCert } from '../controller'; import { authAppByTmbId } from '../app/auth'; import { ManagePermissionVal, @@ -7,6 +6,7 @@ import { import type { EvaluationSchemaType } from '@fastgpt/global/core/app/evaluation/type'; import type { AuthModeType } from '../type'; import { MongoEvaluation } from '../../../core/app/evaluation/evalSchema'; +import { parseHeaderCert } from '../auth/common'; export const authEval = async ({ evalId, diff --git a/packages/service/support/permission/inheritPermission.ts b/packages/service/support/permission/inheritPermission.ts index 2ee7d93719d3..c705f22639e6 100644 --- a/packages/service/support/permission/inheritPermission.ts +++ b/packages/service/support/permission/inheritPermission.ts @@ -1,11 +1,22 @@ import type { ParentIdType } from '@fastgpt/global/common/parentFolder/type'; -import type { RequireOnlyOne } from '@fastgpt/global/common/type/utils'; -import type { PerResourceTypeEnum } from '@fastgpt/global/support/permission/constant'; -import type { PermissionValueType } from '@fastgpt/global/support/permission/type'; -import type { ClientSession, Model } from 'mongoose'; +import { + ManageRoleVal, + NullPermissionVal, + OwnerRoleVal, + type PerResourceTypeEnum +} from '@fastgpt/global/support/permission/constant'; +import type { ResourcePermissionType } from '@fastgpt/global/support/permission/type'; import { mongoSessionRun } from '../../common/mongo/sessionRun'; -import { getResourceClbsAndGroups } from './controller'; +import { getResourceOwnedClbs } from './controller'; import { MongoResourcePermission } from './schema'; +import type { ClientSession, Model, AnyBulkWriteOperation } from '../../common/mongo'; +import { + getCollaboratorId, + mergeCollaboratorList, + sumPer +} from '@fastgpt/global/support/permission/utils'; +import type { CollaboratorItemType } from '@fastgpt/global/support/permission/collaborator'; +import { pickCollaboratorIdFields } from './utils'; export type SyncChildrenPermissionResourceType = { _id: string; @@ -13,15 +24,10 @@ export type SyncChildrenPermissionResourceType = { teamId: string; parentId?: ParentIdType; }; -export type UpdateCollaboratorItem = { - permission: PermissionValueType; -} & RequireOnlyOne<{ - tmbId: string; - groupId: string; - orgId: string; -}>; - -// sync the permission to all children folders. + +/** + * sync the permission to all children folders. + */ export async function syncChildrenPermission({ resource, folderTypeList, @@ -29,7 +35,7 @@ export async function syncChildrenPermission({ resourceModel, session, - collaborators + collaborators: latestClbList }: { resource: SyncChildrenPermissionResourceType; @@ -42,55 +48,155 @@ export async function syncChildrenPermission({ // should be provided when inheritPermission is true session: ClientSession; - collaborators?: UpdateCollaboratorItem[]; + collaborators: CollaboratorItemType[]; }) { // only folder has permission const isFolder = folderTypeList.includes(resource.type); + const teamId = resource.teamId; + // If the 'root' is not a folder, which means the 'root' has no children, no need to sync. if (!isFolder) return; - // get all folders and the resource permission of the app + // get all the resource permission of the app const allFolders = await resourceModel .find( { - teamId: resource.teamId, - type: { $in: folderTypeList }, - inheritPermission: true + teamId, + inheritPermission: true, + type: { + $in: folderTypeList + } }, '_id parentId' ) .lean() .session(session); - // bfs to get all children + const allClbs = await MongoResourcePermission.find({ + resourceType, + teamId, + resourceId: { + $in: allFolders.map((folder) => folder._id) + } + }) + .lean() + .session(session); + + /** ResourceMap */ + const resourceMap = new Map(); + /** parentChildrenMap */ + const parentChildrenMap = new Map(); + + // init the map + allFolders.forEach((resource) => { + resourceMap.set(resource._id, resource); + const parentId = String(resource.parentId); + if (!parentChildrenMap.has(parentId)) { + parentChildrenMap.set(parentId, []); + } + parentChildrenMap.get(parentId)!.push(resource); + }); + + /** resourceIdPermissionMap + * save the clb virtual state, not the real state at present in the DB. + */ + const resourceIdClbMap = new Map(); + + // Initialize the resourceIdPermissionMap + for (const clb of allClbs) { + const resourceId = clb.resourceId; + const arr = resourceIdClbMap.get(resourceId); + if (!arr) { + resourceIdClbMap.set(resourceId, [clb]); + } else { + arr.push(clb); + } + } + + // BFS to get all children const queue = [String(resource._id)]; - const children: string[] = []; + const ops: AnyBulkWriteOperation[] = []; + const latestClbMap = new Map(latestClbList.map((clb) => [getCollaboratorId(clb), { ...clb }])); + while (queue.length) { - const parentId = queue.shift(); - const folderChildren = allFolders.filter( - (folder) => String(folder.parentId) === String(parentId) - ); - children.push(...folderChildren.map((folder) => folder._id)); - queue.push(...folderChildren.map((folder) => folder._id)); - } - if (!children.length) return; + const parentId = String(queue.shift()); + const _children = parentChildrenMap.get(parentId) || []; + if (_children.length === 0) continue; + for (const child of _children) { + // 1. get parent's permission and what permission I have. + const parentClbs = resourceIdClbMap.get(String(child.parentId)) || []; + const myClbs = resourceIdClbMap.get(child._id) || []; + const myClbsIdSet = new Set(myClbs.map((clb) => getCollaboratorId(clb))); - // sync the resource permission - if (collaborators) { - // Update the collaborators of all children - for await (const childId of children) { - await syncCollaborators({ - resourceType, - session, - collaborators, - teamId: resource.teamId, - resourceId: childId - }); + // add or update + for (const latestClb of latestClbList) { + if (latestClb.permission === OwnerRoleVal) { + continue; + } + if (!myClbsIdSet.has(getCollaboratorId(latestClb))) { + ops.push({ + insertOne: { + document: { + resourceId: child._id, + resourceType, + teamId, + permission: latestClb.permission, + ...pickCollaboratorIdFields(latestClb) + } as ResourcePermissionType + } + }); + } else { + const myclb = myClbs.find( + (clb) => getCollaboratorId(latestClb) === getCollaboratorId(clb) + )!; + ops.push({ + updateOne: { + filter: { + resourceId: child._id, + teamId, + ...pickCollaboratorIdFields(latestClb), + resourceType + }, + update: { + permission: sumPer(myclb.permission, latestClb.permission) + } + } + }); + } + } + + // delele + for (const myClb of myClbs) { + const parentClb = parentClbs.find( + (clb) => getCollaboratorId(clb) === getCollaboratorId(myClb) + ); + // the new collaborators doesnt have it, and the permission is same. + // remove it + if ( + !latestClbMap.get(getCollaboratorId(myClb)) && + parentClb && + myClb.permission === parentClb.permission + ) { + ops.push({ + deleteOne: { + filter: { + resourceId: child._id, + teamId, + ...pickCollaboratorIdFields(myClb), + resourceType + } + } + }); + } + } + queue.push(child._id); } } + await MongoResourcePermission.bulkWrite(ops, { session }); + return; } -/* Resume the inherit permission of the resource. +/** Resume the inherit permission of the resource. 1. Folder: Sync parent's defaultPermission and clbs, and sync its children. 2. Resource: Sync parent's defaultPermission, and delete all its clbs. */ @@ -108,51 +214,63 @@ export async function resumeInheritPermission({ session?: ClientSession; }) { const isFolder = folderTypeList.includes(resource.type); + // Folder resource, need to sync children + const [parentClbs, oldMyClbs] = await Promise.all([ + getResourceOwnedClbs({ + resourceId: resource.parentId, + teamId: resource.teamId, + resourceType + }), + getResourceOwnedClbs({ + resourceId: resource._id, + teamId: resource.teamId, + resourceType + }) + ]); - const fn = async (session: ClientSession) => { - // update the resource permission - await resourceModel.updateOne( - { - _id: resource._id - }, - { - inheritPermission: true - }, - { session } - ); + const parentOwner = parentClbs.find((clb) => clb.permission === OwnerRoleVal); - // Folder resource, need to sync children - if (isFolder) { - const parentClbsAndGroups = await getResourceClbsAndGroups({ - resourceId: resource.parentId, - teamId: resource.teamId, - resourceType, - session - }); + const collaborators = mergeCollaboratorList({ + parentClbs, + childClbs: oldMyClbs + }); + const parentManage = collaborators.find( + (clb) => parentOwner?.tmbId && clb.tmbId && parentOwner?.tmbId === clb.tmbId + ); + if (parentManage) parentManage.permission = ManageRoleVal; + + console.log(collaborators); + const fn = async (session: ClientSession) => { + if (isFolder) { // sync self await syncCollaborators({ resourceType, - collaborators: parentClbsAndGroups, + collaborators, teamId: resource.teamId, resourceId: resource._id, session }); // sync children await syncChildrenPermission({ - resource: { - ...resource - }, + resource, resourceModel, folderTypeList, resourceType, session, - collaborators: parentClbsAndGroups + collaborators }); - } else { - // Not folder, delete all clb - await MongoResourcePermission.deleteMany({ resourceId: resource._id }, { session }); } + + await resourceModel.updateOne( + { + _id: resource._id + }, + { + inheritPermission: true + }, + { session } + ); }; if (session) { @@ -162,9 +280,9 @@ export async function resumeInheritPermission({ } } -/* - Delete all the collaborators and then insert the new collaborators. -*/ +/** + * sync parent collaborators to children. + */ export async function syncCollaborators({ resourceType, teamId, @@ -175,30 +293,59 @@ export async function syncCollaborators({ resourceType: PerResourceTypeEnum; teamId: string; resourceId: string; - collaborators: UpdateCollaboratorItem[]; + collaborators: CollaboratorItemType[]; session: ClientSession; }) { - await MongoResourcePermission.deleteMany( - { - resourceType, - teamId, - resourceId - }, - { session } - ); - await MongoResourcePermission.insertMany( - collaborators.map((item) => ({ - teamId: teamId, - resourceId, - resourceType: resourceType, - tmbId: item.tmbId, - groupId: item.groupId, - orgId: item.orgId, - permission: item.permission - })), - { - session, - ordered: true + // should change parent owner permission into manage + collaborators.forEach((clb) => { + if (clb.permission === OwnerRoleVal) { + clb.permission = ManageRoleVal; } + }); + const parentClbMap = new Map(collaborators.map((clb) => [getCollaboratorId(clb), clb])); + const clbsNow = await MongoResourcePermission.find({ + resourceType, + teamId, + resourceId + }) + .lean() + .session(session); + const ops: AnyBulkWriteOperation[] = []; + for (const clb of clbsNow) { + const parentClb = parentClbMap.get(getCollaboratorId(clb)); + const permission = sumPer(parentClb?.permission ?? NullPermissionVal, clb.permission); + ops.push({ + updateOne: { + filter: { + teamId, + resourceId, + resourceType, + ...pickCollaboratorIdFields(clb) + }, + update: { + permission + } + } + }); + } + + const parentHasAndIHaveNot = collaborators.filter( + (clb) => !clbsNow.some((myClb) => getCollaboratorId(clb) === getCollaboratorId(myClb)) ); + + for (const clb of parentHasAndIHaveNot) { + ops.push({ + insertOne: { + document: { + teamId, + resourceId, + resourceType, + ...pickCollaboratorIdFields(clb), + permission: clb.permission + } as ResourcePermissionType + } + }); + } + + await MongoResourcePermission.bulkWrite(ops, { session }); } diff --git a/packages/service/support/permission/memberGroup/controllers.ts b/packages/service/support/permission/memberGroup/controllers.ts index 3d775e11c1be..3620a8b24f1d 100644 --- a/packages/service/support/permission/memberGroup/controllers.ts +++ b/packages/service/support/permission/memberGroup/controllers.ts @@ -1,6 +1,5 @@ import { type MemberGroupSchemaType } from '@fastgpt/global/support/permission/memberGroup/type'; import { MongoGroupMemberModel } from './groupMemberSchema'; -import { parseHeaderCert } from '../controller'; import { MongoMemberGroupModel } from './memberGroupSchema'; import { DefaultGroupName } from '@fastgpt/global/support/user/team/group/constant'; import { type ClientSession } from 'mongoose'; @@ -9,6 +8,7 @@ import { type AuthModeType, type AuthResponseType } from '../type'; import { TeamErrEnum } from '@fastgpt/global/common/error/code/team'; import { TeamPermission } from '@fastgpt/global/support/permission/user/controller'; import { getTmbInfoByTmbId } from '../../user/team/controller'; +import { parseHeaderCert } from '../auth/common'; /** * Get the default group of a team diff --git a/packages/service/support/permission/org/orgSchema.ts b/packages/service/support/permission/org/orgSchema.ts index 94fa531e466f..54acb4820d83 100644 --- a/packages/service/support/permission/org/orgSchema.ts +++ b/packages/service/support/permission/org/orgSchema.ts @@ -4,6 +4,7 @@ import type { OrgSchemaType } from '@fastgpt/global/support/user/team/org/type'; import { connectionMongo, getMongoModel } from '../../../common/mongo'; import { OrgMemberCollectionName } from './orgMemberSchema'; import { getNanoid } from '@fastgpt/global/common/string/tools'; +import { DEFAULT_ORG_AVATAR } from '@fastgpt/global/common/system/constants'; const { Schema } = connectionMongo; export const OrgSchema = new Schema( @@ -29,7 +30,9 @@ export const OrgSchema = new Schema( type: String, required: true }, - avatar: String, + avatar: { + type: String + }, description: String, updateTime: { type: Date, diff --git a/packages/service/support/permission/publish/authLink.ts b/packages/service/support/permission/publish/authLink.ts index 269ce4854aa0..479c3babbc47 100644 --- a/packages/service/support/permission/publish/authLink.ts +++ b/packages/service/support/permission/publish/authLink.ts @@ -1,11 +1,11 @@ import { type AppDetailType } from '@fastgpt/global/core/app/type'; import { type OutlinkAppType, type OutLinkSchema } from '@fastgpt/global/support/outLink/type'; -import { parseHeaderCert } from '../controller'; import { MongoOutLink } from '../../outLink/schema'; import { OutLinkErrEnum } from '@fastgpt/global/common/error/code/outLink'; import { OwnerPermissionVal } from '@fastgpt/global/support/permission/constant'; import { authAppByTmbId } from '../app/auth'; import { type AuthModeType, type AuthResponseType } from '../type'; +import { parseHeaderCert } from '../auth/common'; /* crud outlink permission */ export async function authOutLinkCrud({ diff --git a/packages/service/support/permission/schema.ts b/packages/service/support/permission/schema.ts index ae53c7da1d2e..56c5e602d85c 100644 --- a/packages/service/support/permission/schema.ts +++ b/packages/service/support/permission/schema.ts @@ -34,11 +34,18 @@ export const ResourcePermissionSchema = new Schema({ enum: Object.values(PerResourceTypeEnum), required: true }, + /** + * The **Role** of the object to the resource. + */ permission: { type: Number, required: true }, - // Resrouce ID: App or DataSet or any other resource type. + /** + * Optional. Only be set when the resource is *inherited* from the parent resource. + * For recording the self permission. When cancel the inheritance, it will overwrite the permission property and set to `unset`. + */ + // Resource ID: App or DataSet or any other resource type. // It is null if the resourceType is team. resourceId: { type: Schema.Types.ObjectId diff --git a/packages/service/support/permission/user/auth.ts b/packages/service/support/permission/user/auth.ts index 5029008fc13a..88dd38555991 100644 --- a/packages/service/support/permission/user/auth.ts +++ b/packages/service/support/permission/user/auth.ts @@ -1,11 +1,10 @@ import { type TeamTmbItemType } from '@fastgpt/global/support/user/team/type'; -import { parseHeaderCert } from '../controller'; import { getTmbInfoByTmbId } from '../../user/team/controller'; import { TeamErrEnum } from '@fastgpt/global/common/error/code/team'; import { type AuthModeType, type AuthResponseType } from '../type'; import { NullPermissionVal } from '@fastgpt/global/support/permission/constant'; import { TeamPermission } from '@fastgpt/global/support/permission/user/controller'; -import { authCert } from '../auth/common'; +import { authCert, parseHeaderCert } from '../auth/common'; import { MongoUser } from '../../user/schema'; import { ERROR_ENUM } from '@fastgpt/global/common/error/errorCode'; import { type ApiRequestProps } from '../../../type/next'; diff --git a/packages/service/support/permission/utils.ts b/packages/service/support/permission/utils.ts new file mode 100644 index 000000000000..eb4a70b80fe3 --- /dev/null +++ b/packages/service/support/permission/utils.ts @@ -0,0 +1,9 @@ +import type { CollaboratorIdType } from '@fastgpt/global/support/permission/collaborator'; + +export const pickCollaboratorIdFields = (clb: CollaboratorIdType) => { + return { + ...(clb.tmbId && { tmbId: clb.tmbId }), + ...(clb.groupId && { groupId: clb.groupId }), + ...(clb.orgId && { orgId: clb.orgId }) + }; +}; diff --git a/packages/service/support/user/team/controller.ts b/packages/service/support/user/team/controller.ts index a399bd35884d..fa8ed440b390 100644 --- a/packages/service/support/user/team/controller.ts +++ b/packages/service/support/user/team/controller.ts @@ -8,7 +8,7 @@ import { import { MongoTeamMember } from './teamMemberSchema'; import { MongoTeam } from './teamSchema'; import { type UpdateTeamProps } from '@fastgpt/global/support/user/team/controller'; -import { getResourcePermission } from '../../permission/controller'; +import { getTmbPermission } from '../../permission/controller'; import { PerResourceTypeEnum } from '@fastgpt/global/support/permission/constant'; import { TeamPermission } from '@fastgpt/global/support/permission/user/controller'; import { TeamDefaultRoleVal } from '@fastgpt/global/support/permission/user/constant'; @@ -26,7 +26,7 @@ async function getTeamMember(match: Record): Promise( w={selectItem.iconSize ?? '1rem'} /> )} - {selectItem?.alias || selectItem?.label || placeholder} + { + + {selectItem?.alias || selectItem?.label || placeholder} + + } )} diff --git a/packages/web/components/common/Radio/LeftRadio.tsx b/packages/web/components/common/Radio/LeftRadio.tsx index 95b19e0eacf5..6006d959e2c4 100644 --- a/packages/web/components/common/Radio/LeftRadio.tsx +++ b/packages/web/components/common/Radio/LeftRadio.tsx @@ -1,5 +1,5 @@ -import React from 'react'; -import { Box, Flex, useTheme, Grid, type GridProps, HStack } from '@chakra-ui/react'; +import React, { useCallback } from 'react'; +import { Box, Flex, Grid, type GridProps, HStack } from '@chakra-ui/react'; import { useTranslation } from 'next-i18next'; import QuestionTip from '../MyTooltip/QuestionTip'; @@ -16,57 +16,83 @@ type Props = Omit & { defaultBg?: string; activeBg?: string; onChange: (e: T) => void; + isDisabled?: boolean; }; const LeftRadio = ({ list, value, - align = 'flex-top', + align = 'center', px = 3.5, py = 4, defaultBg = 'myGray.50', activeBg = 'primary.50', onChange, + isDisabled = false, ...props }: Props) => { const { t } = useTranslation(); - const theme = useTheme(); + + const getBoxStyle = useCallback( + (isActive: boolean) => { + const baseStyle = { + px, + py, + border: 'base', + borderWidth: '1px', + borderRadius: 'md' + }; + + if (isActive) { + return { + ...baseStyle, + borderColor: 'primary.400', + bg: activeBg, + boxShadow: 'focus', + cursor: 'pointer', + opacity: 1 + }; + } + if (isDisabled) { + return { + ...baseStyle, + bg: 'myWhite.300', + borderColor: 'myGray.200', + color: 'myGray.500', + cursor: 'not-allowed', + opacity: 0.6 + }; + } + return { + ...baseStyle, + bg: defaultBg, + _hover: { borderColor: 'primary.300' }, + cursor: 'pointer', + opacity: 1 + }; + }, + [activeBg, defaultBg, isDisabled, px, py] + ); return ( - {list.map((item) => ( - 1 ? 'primary.400' : '', - bg: activeBg, - boxShadow: list.length > 1 ? 'focus' : 'none' - } - : { - bg: defaultBg, - _hover: { - borderColor: 'primary.300' - } - })} - onClick={() => onChange(item.value)} - > - {/* Circle */} - - {list.length > 1 && ( + {list.map((item) => { + const isActive = value === item.value; + return ( + !isDisabled && onChange(item.value)} + {...getBoxStyle(isActive)} + > + + {/* Circle */} @@ -74,52 +100,59 @@ const LeftRadio = ({ w={'100%'} h={'100%'} borderWidth={'1px'} - borderColor={value === item.value ? 'primary.600' : 'borderColor.high'} - bg={value === item.value ? 'primary.1' : 'transparent'} borderRadius={'50%'} alignItems={'center'} justifyContent={'center'} + {...(isActive + ? { + borderColor: 'primary.600', + bg: 'primary.1' + } + : { + borderColor: 'borderColor.high', + bg: 'transparent' + })} > - )} - - {typeof item.title === 'string' ? ( - - {t(item.title as any)} - {!!item.tooltip && } - - ) : ( - item.title - )} + + {typeof item.title === 'string' ? ( + + {t(item.title as any)} + {!!item.tooltip && } + + ) : ( + item.title + )} - {!!item.desc && ( - - {t(item.desc as any)} - - )} - - - {item?.children && ( - - {item?.children} - - )} - - ))} + {!!item.desc && ( + + {t(item.desc as any)} + + )} + + + {item?.children && ( + + {item?.children} + + )} + + ); + })} ); }; diff --git a/packages/web/components/common/Textarea/PromptEditor/Editor.tsx b/packages/web/components/common/Textarea/PromptEditor/Editor.tsx index 4f4a0433c7a1..f11a8ff34afb 100644 --- a/packages/web/components/common/Textarea/PromptEditor/Editor.tsx +++ b/packages/web/components/common/Textarea/PromptEditor/Editor.tsx @@ -15,6 +15,7 @@ import { HistoryPlugin } from '@lexical/react/LexicalHistoryPlugin'; import { OnChangePlugin } from '@lexical/react/LexicalOnChangePlugin'; import { ListPlugin } from '@lexical/react/LexicalListPlugin'; import { CheckListPlugin } from '@lexical/react/LexicalCheckListPlugin'; +import { TabIndentationPlugin } from '@lexical/react/LexicalTabIndentationPlugin'; import LexicalErrorBoundary from '@lexical/react/LexicalErrorBoundary'; import { HeadingNode, QuoteNode } from '@lexical/rich-text'; import { ListItemNode, ListNode } from '@lexical/list'; @@ -219,7 +220,6 @@ export default function Editor({ )} {variableLabels.length > 0 && } - { const rootElement = editor.getRootElement(); @@ -232,11 +232,12 @@ export default function Editor({ {isRichText && ( <> - {/* + + + - */} - - {/* */} + + )} diff --git a/packages/web/components/common/Textarea/PromptEditor/plugins/ListExitPlugin/index.tsx b/packages/web/components/common/Textarea/PromptEditor/plugins/ListExitPlugin/index.tsx index a4359493b95f..032ca9d2bfac 100644 --- a/packages/web/components/common/Textarea/PromptEditor/plugins/ListExitPlugin/index.tsx +++ b/packages/web/components/common/Textarea/PromptEditor/plugins/ListExitPlugin/index.tsx @@ -70,7 +70,7 @@ export default function ListExitPlugin(): JSX.Element | null { } const anchorNode = selection.anchor.getNode(); - const listItemNode = anchorNode.getParent(); + const listItemNode = $isListItemNode(anchorNode) ? anchorNode : anchorNode.getParent(); if ($isListItemNode(listItemNode)) { // Check if cursor is at the beginning of an empty list item diff --git a/packages/web/components/common/Textarea/PromptEditor/type.d.ts b/packages/web/components/common/Textarea/PromptEditor/type.d.ts index 66cf2391612c..d62f4b738ea2 100644 --- a/packages/web/components/common/Textarea/PromptEditor/type.d.ts +++ b/packages/web/components/common/Textarea/PromptEditor/type.d.ts @@ -23,3 +23,88 @@ export type EditorVariableLabelPickerType = { }; export type FormPropsType = Omit; + +// Lexical editor node types +export type BaseEditorNode = { + type: string; + version: number; +}; + +export type TextEditorNode = BaseEditorNode & { + type: 'text'; + text: string; + detail: number; + format: number; + mode: string; + style: string; +}; + +export type LineBreakEditorNode = BaseEditorNode & { + type: 'linebreak'; +}; + +export type VariableLabelEditorNode = BaseEditorNode & { + type: 'variableLabel'; + variableKey: string; +}; + +export type VariableEditorNode = BaseEditorNode & { + type: 'Variable'; + variableKey: string; +}; + +export type TabEditorNode = BaseEditorNode & { + type: 'tab'; +}; + +export type ChildEditorNode = + | TextEditorNode + | LineBreakEditorNode + | VariableLabelEditorNode + | VariableEditorNode + | TabEditorNode; + +export type ParagraphEditorNode = BaseEditorNode & { + type: 'paragraph'; + children: ChildEditorNode[]; + direction: string; + format: string; + indent: number; +}; + +export type ListItemEditorNode = BaseEditorNode & { + type: 'listitem'; + children: Array; + direction: string | null; + format: string; + indent: number; + value: number; +}; + +export type ListEditorNode = BaseEditorNode & { + type: 'list'; + children: ListItemEditorNode[]; + direction: string | null; + format: string; + indent: number; + listType: 'bullet' | 'number'; + start: number; + tag: 'ul' | 'ol'; +}; + +export type EditorState = { + root: { + type: 'root'; + children: Array; + direction: string; + format: string; + indent: number; + } & BaseEditorNode; +}; + +export type ListItemInfo = { + type: 'bullet' | 'number'; + text: string; + indent: number; + numberValue?: number; +}; diff --git a/packages/web/components/common/Textarea/PromptEditor/utils.ts b/packages/web/components/common/Textarea/PromptEditor/utils.ts index e3603800e3e0..44cc284525fb 100644 --- a/packages/web/components/common/Textarea/PromptEditor/utils.ts +++ b/packages/web/components/common/Textarea/PromptEditor/utils.ts @@ -6,12 +6,19 @@ * */ -import type { DecoratorNode, Klass, LexicalEditor, LexicalNode } from 'lexical'; +import type { Klass, LexicalEditor, LexicalNode } from 'lexical'; import type { EntityMatch } from '@lexical/text'; import { $createTextNode, $isTextNode, TextNode } from 'lexical'; import { useCallback } from 'react'; import type { VariableLabelNode } from './plugins/VariableLabelPlugin/node'; import type { VariableNode } from './plugins/VariablePlugin/node'; +import type { + ListItemEditorNode, + ListEditorNode, + ParagraphEditorNode, + EditorState, + ListItemInfo +} from './type'; export function registerLexicalTextEntity( editor: LexicalEditor, @@ -175,31 +182,148 @@ export function registerLexicalTextEntity { + const trimmed = line.trimStart(); + const indentLevel = Math.floor((line.length - trimmed.length) / 2); + + const bulletMatch = trimmed.match(/^- (.*)$/); + if (bulletMatch) { + return { type: 'bullet', text: bulletMatch[1], indent: indentLevel }; + } + + const numberMatch = trimmed.match(/^(\d+)\. (.*)$/); + if (numberMatch) { + return { + type: 'number', + text: numberMatch[2], + indent: indentLevel, + numberValue: parseInt(numberMatch[1]) + }; + } + + return { type: 'paragraph', text: trimmed, indent: indentLevel }; +}; + +const buildListStructure = (items: ListItemInfo[]) => { + const result: ListEditorNode[] = []; + + let i = 0; + while (i < items.length) { + const currentListType = items[i].type; + const currentIndent = items[i].indent; + const currentListItems: ListItemEditorNode[] = []; + + // Collect consecutive items of the same type + while (i < items.length && items[i].type === currentListType) { + const listItem: ListItemEditorNode = { + children: [ + { + detail: 0, + format: 0, + mode: 'normal', + style: '', + text: items[i].text, + type: 'text' as const, + version: 1 + } + ], + direction: 'ltr', + format: '', + indent: 0, + type: 'listitem' as const, + version: 1, + value: items[i].numberValue || 1 + }; + + // Collect nested items + const nestedItems: ListItemInfo[] = []; + let j = i + 1; + while (j < items.length && items[j].indent > currentIndent) { + nestedItems.push(items[j]); + j++; + } + + // recursively build nested lists and add them to the current item's children + if (nestedItems.length > 0) { + const nestedLists = buildListStructure(nestedItems); + listItem.children.push(...nestedLists); + } + + currentListItems.push(listItem); + i = j; + } + + result.push({ + children: currentListItems, + direction: 'ltr', + format: '', + indent: 0, + type: 'list' as const, + version: 1, + listType: currentListType, + start: 1, + tag: currentListType === 'bullet' ? 'ul' : ('ol' as const) + }); + } + + return result; +}; + +export const textToEditorState = (text = '') => { + const lines = text.split('\n'); + const children: Array = []; + + let i = 0; + while (i < lines.length) { + const parsed = parseTextLine(lines[i]); + + if (parsed.type === 'paragraph') { + children.push({ + children: [ + { + detail: 0, + format: 0, + mode: 'normal', + style: '', + text: parsed.text, + type: 'text', + version: 1 + } + ], + direction: 'ltr', + format: '', + indent: parsed.indent, + type: 'paragraph', + version: 1 + }); + i++; + } else { + const listItems: ListItemInfo[] = []; + + while (i < lines.length) { + const currentParsed = parseTextLine(lines[i]); + if (currentParsed.type === 'paragraph') { + break; + } + listItems.push({ + type: currentParsed.type as 'bullet' | 'number', + text: currentParsed.text, + indent: currentParsed.indent, + numberValue: currentParsed.numberValue + }); + i++; + } + + // build nested lists and add to children + const lists = buildListStructure(listItems) as ListEditorNode[]; + children.push(...lists); + } + } return JSON.stringify({ root: { - children: paragraph.map((p) => { - return { - children: [ - { - detail: 0, - format: 0, - mode: 'normal', - style: '', - text: p, - type: 'text', - version: 1 - } - ], - direction: 'ltr', - format: '', - indent: 0, - type: 'paragraph', - version: 1 - }; - }), + children: children, direction: 'ltr', format: '', indent: 0, @@ -207,30 +331,9 @@ export function textToEditorState(text = '') { version: 1 } }); -} - -const varRegex = /\{\{([a-zA-Z_][a-zA-Z0-9_]*)\}\}/g; -export const getVars = (value: string) => { - if (!value) return []; - const keys = - value - .match(varRegex) - ?.map((item) => { - return item.replace('{{', '').replace('}}', ''); - }) - .filter((key) => key.length <= 10) || []; - const keyObj: Record = {}; - // remove duplicate keys - const res: string[] = []; - keys.forEach((key) => { - if (keyObj[key]) return; - - keyObj[key] = true; - res.push(key); - }); - return res; }; +// menu text match export type MenuTextMatch = { leadOffset: number; matchingString: string; @@ -266,22 +369,102 @@ export function useBasicTypeaheadTriggerMatch( ); } -export function editorStateToText(editor: LexicalEditor) { - const editorStateTextString: string[] = []; - const paragraphs = editor.getEditorState().toJSON().root.children; - paragraphs.forEach((paragraph: any) => { - const children = paragraph.children || []; - const paragraphText: string[] = []; - children.forEach((child: any) => { - if (child.type === 'linebreak') { - paragraphText.push('\n'); - } else if (child.text) { - paragraphText.push(child.text); - } else if (child.type === 'variableLabel' || child.type === 'Variable') { - paragraphText.push(child.variableKey); - } +// editor state to text +const processListItem = ({ + listItem, + listType, + index, + indentLevel +}: { + listItem: ListItemEditorNode; + listType: 'bullet' | 'number'; + index: number; + indentLevel: number; +}) => { + const results = []; + + const itemText: string[] = []; + const nestedLists: ListEditorNode[] = []; + + // Separate text and nested lists + listItem.children.forEach((child) => { + if (child.type === 'linebreak') { + itemText.push('\n'); + } else if (child.type === 'text') { + itemText.push(child.text); + } else if (child.type === 'tab') { + itemText.push(' '); + } else if (child.type === 'variableLabel' || child.type === 'Variable') { + itemText.push(child.variableKey); + } else if (child.type === 'list') { + nestedLists.push(child); + } + }); + + // Add prefix and indent + const itemTextString = itemText.join('').trim(); + const indent = ' '.repeat(indentLevel); + const prefix = listType === 'bullet' ? '- ' : `${index + 1}. `; + results.push(indent + prefix + itemTextString); + + // Handle nested lists + nestedLists.forEach((nestedList) => { + const nestedResults = processList({ + list: nestedList, + indentLevel: indentLevel + 1 }); - editorStateTextString.push(paragraphText.join('')); + results.push(...nestedResults); + }); + + return results; +}; +const processList = ({ list, indentLevel = 0 }: { list: ListEditorNode; indentLevel?: number }) => { + const results: string[] = []; + + list.children.forEach((listItem, index: number) => { + if (listItem.type === 'listitem') { + const itemResults = processListItem({ + listItem, + listType: list.listType, + index, + indentLevel + }); + results.push(...itemResults); + } + }); + + return results; +}; +export const editorStateToText = (editor: LexicalEditor) => { + const editorStateTextString: string[] = []; + const editorState = editor.getEditorState().toJSON() as EditorState; + const paragraphs = editorState.root.children; + + paragraphs.forEach((paragraph) => { + if (paragraph.type === 'list') { + const listResults = processList({ list: paragraph }); + editorStateTextString.push(...listResults); + } else if (paragraph.type === 'paragraph') { + const children = paragraph.children; + const paragraphText: string[] = []; + + const indentSpaces = ' '.repeat(paragraph.indent || 0); + + children.forEach((child) => { + if (child.type === 'linebreak') { + paragraphText.push('\n'); + } else if (child.type === 'text') { + paragraphText.push(child.text); + } else if (child.type === 'tab') { + paragraphText.push(' '); + } else if (child.type === 'variableLabel' || child.type === 'Variable') { + paragraphText.push(child.variableKey); + } + }); + + const finalText = paragraphText.join(''); + editorStateTextString.push(indentSpaces + finalText); + } }); return editorStateTextString.join('\n'); -} +}; diff --git a/packages/web/hooks/usePagination.tsx b/packages/web/hooks/usePagination.tsx index 1e70eff6ff9f..e8f5f87e110c 100644 --- a/packages/web/hooks/usePagination.tsx +++ b/packages/web/hooks/usePagination.tsx @@ -26,6 +26,7 @@ import { import { type PaginationProps, type PaginationResponse } from '../common/fetch/type'; import MyMenu from '../components/common/MyMenu'; import { useSystem } from './useSystem'; +import { useRouter } from 'next/router'; const thresholdVal = 200; @@ -35,19 +36,18 @@ export function usePagination( defaultPageSize = 10, pageSizeOptions: defaultPageSizeOptions, params, - defaultRequest = true, type = 'button', onChange, refreshDeps, scrollLoadType = 'bottom', EmptyTip, pollingInterval, - pollingWhenHidden = false + pollingWhenHidden = false, + storeToQuery = false }: { defaultPageSize?: number; pageSizeOptions?: number[]; params?: DataT; - defaultRequest?: boolean; type?: 'button' | 'scroll'; onChange?: (pageNum: number) => void; refreshDeps?: any[]; @@ -56,15 +56,20 @@ export function usePagination( EmptyTip?: React.JSX.Element; pollingInterval?: number; pollingWhenHidden?: boolean; + storeToQuery?: boolean; } ) { + const router = useRouter(); + let { page = '1' } = router.query as { page: string }; + const numPage = Number(page); + const { toast } = useToast(); const { isPc } = useSystem(); const { t } = useTranslation(); const [isLoading, { setTrue, setFalse }] = useBoolean(false); - const [pageNum, setPageNum] = useState(1); + const [pageNum, setPageNum] = useState(numPage); const [pageSize, setPageSize] = useState(defaultPageSize); const pageSizeOptions = useCreation( () => defaultPageSizeOptions || [10, 20, 50, 100], @@ -76,7 +81,7 @@ export function usePagination( const totalDataLength = useMemo(() => Math.max(total, data.length), [total, data.length]); const isEmpty = total === 0 && !isLoading; - const noMore = data.length >= totalDataLength; + const noMore = data.length > 0 && data.length >= totalDataLength; const fetchData = useMemoizedFn( async (num: number = pageNum, ScrollContainerRef?: RefObject) => { @@ -92,6 +97,16 @@ export function usePagination( }); setPageNum(num); + if (storeToQuery && num !== pageNum) { + router.replace({ + pathname: router.pathname, + query: { + ...router.query, + page: num + } + }); + } + res.total !== undefined && setTotal(res.total); if (type === 'scroll') { @@ -268,7 +283,8 @@ export function usePagination( // Watch scroll position useThrottleEffect( () => { - if (!ref?.current || type !== 'scroll' || noMore || isLoading) return; + if (!ref?.current || type !== 'scroll' || noMore || isLoading || data.length === 0) + return; const { scrollTop, scrollHeight, clientHeight } = ref.current; if ( @@ -313,9 +329,16 @@ export function usePagination( ); // Reload data + const isFirstLoad = useRef(true); const { runAsync: refresh } = useRequest( async () => { - defaultRequest && fetchData(1); + if (isFirstLoad.current) { + isFirstLoad.current = false; + fetchData(numPage); + return; + } + + fetchData(1); }, { manual: false, @@ -323,6 +346,7 @@ export function usePagination( throttleWait: 100 } ); + // Page size refresh useEffect(() => { data.length > 0 && fetchData(); }, [pageSize]); diff --git a/packages/web/i18n/en/account_team.json b/packages/web/i18n/en/account_team.json index b34f9b40d70a..36b54af9e06c 100644 --- a/packages/web/i18n/en/account_team.json +++ b/packages/web/i18n/en/account_team.json @@ -91,7 +91,7 @@ "forbid_hint": "After forbidden, this invitation link will become invalid. This action is irreversible. Are you sure you want to deactivate?", "forbid_success": "Forbid success", "forbidden": "Forbidden", - "link_forbidden": "Forbidden", + "forbidden_tip": "Confirm disabling {{username}}? The member will be marked as 'disabled' and will not be able to log in. Operation data will not be deleted, and resources under the account will be automatically transferred to the team owner.", "group": "group", "group_name": "Group name", "handle_invitation": "Handle Invitation", @@ -113,6 +113,7 @@ "label_sync": "Tag sync", "leave": "Resigned", "leave_team_failed": "Leaving the team exception", + "link_forbidden": "Forbidden", "log_admin_add_plan": "【{{name}}】A package will be added to a team with a team id [{{teamId}}]", "log_admin_add_user": "【{{name}}】Create a user named [{{userName}}]", "log_admin_change_license": "【{{name}}】Changed License", @@ -196,6 +197,7 @@ "log_user": "Operator", "login": "Log in", "manage_member": "Managing members", + "manage_per": "Administrative permissions", "member": "member", "member_group": "Belonging to member group", "move_app": "App location movement", @@ -222,7 +224,6 @@ "relocate_department": "Department Mobile", "remark": "remark", "remove_tip": "Confirm to remove {{username}} from the team? The member will be marked as 'leave'. Operation data will not be deleted, and resources under the account will be automatically transferred to the team owner.", - "forbidden_tip": "Confirm disabling {{username}}? The member will be marked as 'disabled' and will not be able to log in. Operation data will not be deleted, and resources under the account will be automatically transferred to the team owner.", "restore_tip": "Confirm to join the team {{username}}? \nOnly the availability and related permissions of this member account are restored, and the resources under the account cannot be restored.", "restore_tip_title": "Recovery confirmation", "retain_admin_permissions": "Keep administrator rights", diff --git a/packages/web/i18n/en/app.json b/packages/web/i18n/en/app.json index df7f700edcb5..8e682bfcec9f 100644 --- a/packages/web/i18n/en/app.json +++ b/packages/web/i18n/en/app.json @@ -176,7 +176,7 @@ "module.type": "\"{{type}}\" type\n{{description}}", "modules.Title is required": "Module name cannot be empty", "month.unit": "Day", - "move.hint": "After moving, the selected application/folder will inherit the permission settings of the new folder, and the original permission settings will become invalid.", + "move.hint": "After moving, the selected app/folder will inherit the permission settings for the new folder.", "move_app": "Move Application", "no_mcp_tools_list": "No data yet, the MCP address needs to be parsed first", "node_not_intro": "This node is not introduced", @@ -190,7 +190,7 @@ "pdf_enhance_parse": "PDF enhancement analysis", "pdf_enhance_parse_price": "{{price}}Points/page", "pdf_enhance_parse_tips": "Calling PDF recognition model for parsing, you can convert it into Markdown and retain pictures in the document. At the same time, you can also identify scanned documents, which will take a long time to identify them.", - "permission.des.manage": "Based on write permissions, you can configure publishing channels, view conversation logs, and assign permissions to the application.", + "permission.des.manage": "Can configure publishing channels, view logs, and assign application permissions", "permission.des.read": "Use the app to have conversations", "permission.des.readChatLog": "Can view chat logs", "permission.des.write": "Can view and edit apps", diff --git a/packages/web/i18n/en/common.json b/packages/web/i18n/en/common.json index eba6175fc943..05ad9619919f 100644 --- a/packages/web/i18n/en/common.json +++ b/packages/web/i18n/en/common.json @@ -966,7 +966,7 @@ "permission.Private Tip": "Only Available to Yourself", "permission.Public": "Team", "permission.Public Tip": "Available to All Team Members", - "permission.Remove InheritPermission Confirm": "This operation will invalidate permission inheritance. Proceed?", + "permission.Remove InheritPermission Confirm": "This modification conflicts with inheritance permissions, which will cause permission inheritance to be invalid. Will it be carried out?", "permission.Resume InheritPermission Confirm": "Resume inheriting permissions from the parent folder?", "permission.Resume InheritPermission Failed": "Resume Failed", "permission.Resume InheritPermission Success": "Resume Successful", @@ -976,6 +976,7 @@ "permission.change_owner_success": "Ownership Transferred Successfully", "permission.change_owner_tip": "Your permissions will not be retained after the transfer", "permission.change_owner_to": "Transfer to", + "permission.common_member": "Common members", "permission.manager": "administrator", "permission.read": "Read permission", "permission.write": "write permission", diff --git a/packages/web/i18n/en/dataset.json b/packages/web/i18n/en/dataset.json index c30dc82bfcf4..88e091a9b132 100644 --- a/packages/web/i18n/en/dataset.json +++ b/packages/web/i18n/en/dataset.json @@ -133,7 +133,7 @@ "llm_paragraph_mode_force_desc": "Force the use of the model to automatically identify paragraphs and ignore paragraphs in the original text (if any)", "loading": "Loading...", "max_chunk_size": "Maximum chunk size", - "move.hint": "After moving, the selected knowledge base/folder will inherit the permission settings of the new folder, and the original permission settings will become invalid.", + "move.hint": "After moving, the selected knowledge base/folder will inherit the permission settings for the new folder.", "noChildren": "No subdirectories", "noSelectedFolder": "No selected folder", "noSelectedId": "No selected ID", diff --git a/packages/web/i18n/en/user.json b/packages/web/i18n/en/user.json index 5dae7009677f..6b488c9d2d56 100644 --- a/packages/web/i18n/en/user.json +++ b/packages/web/i18n/en/user.json @@ -21,7 +21,6 @@ "delete.admin_success": "Admin Deleted Successfully", "delete.failed": "Delete failed", "delete.success": "Delete successfully", - "has_chosen": "Selected", "login.Dingtalk": "DingTalk Login", "login.error": "Login Error", "login.password_condition": "Password can be up to 60 characters", diff --git a/packages/web/i18n/zh-CN/account_team.json b/packages/web/i18n/zh-CN/account_team.json index 15b7598be30a..67f3c37bd1b4 100644 --- a/packages/web/i18n/zh-CN/account_team.json +++ b/packages/web/i18n/zh-CN/account_team.json @@ -93,7 +93,7 @@ "forbid_hint": "停用后,该邀请链接将失效。 该操作不可撤销,是否确认停用?", "forbid_success": "停用成功", "forbidden": "停用", - "link_forbidden": "禁用", + "forbidden_tip": "确认将 {{username}} 禁用?成员将被标记为“禁用”并无法登录,不删除操作数据,账号下资源自动转让给团队所有者。", "group": "群组", "group_name": "群组名称", "handle_invitation": "处理团队邀请", @@ -115,6 +115,7 @@ "label_sync": "标签同步", "leave": "离开", "leave_team_failed": "离开团队异常", + "link_forbidden": "禁用", "log_admin_add_plan": "【{{name}}】将给团队id为【{{teamId}}】的团队添加了套餐", "log_admin_add_user": "【{{name}}】创建了一个名为【{{userName}}】的用户", "log_admin_change_license": "【{{name}}】变更了License", @@ -200,6 +201,7 @@ "log_user": "操作人员", "login": "登录", "manage_member": "管理成员", + "manage_per": "管理权限", "member": "成员", "member_group": "所属群组", "move_app": "应用位置移动", @@ -226,7 +228,6 @@ "relocate_department": "部门移动", "remark": "备注", "remove_tip": "确认将 {{username}} 移出团队?成员将被标记为“离开”,不删除操作数据,账号下资源自动转让给团队所有者。", - "forbidden_tip": "确认将 {{username}} 禁用?成员将被标记为“禁用”并无法登录,不删除操作数据,账号下资源自动转让给团队所有者。", "restore_tip": "确认将 {{username}} 加入团队吗?仅恢复该成员账号可用性及相关权限,无法恢复账号下资源。", "restore_tip_title": "恢复确认", "retain_admin_permissions": "保留管理员权限", diff --git a/packages/web/i18n/zh-CN/app.json b/packages/web/i18n/zh-CN/app.json index a8fa488fc31b..cea7ed34d281 100644 --- a/packages/web/i18n/zh-CN/app.json +++ b/packages/web/i18n/zh-CN/app.json @@ -185,7 +185,7 @@ "module.type": "\"{{type}}\"类型\n{{description}}", "modules.Title is required": "模块名不能为空", "month.unit": "号", - "move.hint": "移动后,所选应用/文件夹将继承新文件夹的权限设置,原先的权限设置失效。", + "move.hint": "移动后,所选应用/文件夹将继承新文件夹的权限设置。", "move_app": "移动应用", "no_mcp_tools_list": "暂无数据,需先解析 MCP 地址", "node_not_intro": "这个节点没有介绍", @@ -199,7 +199,7 @@ "pdf_enhance_parse": "PDF增强解析", "pdf_enhance_parse_price": "{{price}}积分/页", "pdf_enhance_parse_tips": "调用 PDF 识别模型进行解析,可以将其转换成 Markdown 并保留文档中的图片,同时也可以对扫描件进行识别,识别时间较长。", - "permission.des.manage": "写权限基础上,可配置发布渠道、查看对话日志、分配该应用权限", + "permission.des.manage": "可配置发布渠道、查看日志、分配应用权限", "permission.des.read": "可使用该应用进行对话", "permission.des.readChatLog": "可查看对话日志", "permission.des.write": "可查看和编辑应用", diff --git a/packages/web/i18n/zh-CN/common.json b/packages/web/i18n/zh-CN/common.json index 45936b40a616..f79683ad673b 100644 --- a/packages/web/i18n/zh-CN/common.json +++ b/packages/web/i18n/zh-CN/common.json @@ -967,7 +967,7 @@ "permission.Private Tip": "仅自己可用", "permission.Public": "协作", "permission.Public Tip": "团队所有成员可使用", - "permission.Remove InheritPermission Confirm": "此操作会导致权限继承失效,是否进行?", + "permission.Remove InheritPermission Confirm": "此修改与继承权限存在冲突,会导致权限继承失效,是否进行?", "permission.Resume InheritPermission Confirm": "是否恢复为继承父级文件夹的权限?", "permission.Resume InheritPermission Failed": "恢复失败", "permission.Resume InheritPermission Success": "恢复成功", @@ -977,6 +977,7 @@ "permission.change_owner_success": "成功转移所有权", "permission.change_owner_tip": "转移后您的权限不会保留", "permission.change_owner_to": "转移给", + "permission.common_member": "普通成员", "permission.manager": "管理员", "permission.read": "读权限", "permission.write": "写权限", diff --git a/packages/web/i18n/zh-CN/dataset.json b/packages/web/i18n/zh-CN/dataset.json index aa3cb957a8e0..08f17c3afd54 100644 --- a/packages/web/i18n/zh-CN/dataset.json +++ b/packages/web/i18n/zh-CN/dataset.json @@ -133,7 +133,7 @@ "llm_paragraph_mode_force_desc": "强制使用模型自动识别段落,并忽略原文本的段落(如有)", "loading": "加载中...", "max_chunk_size": "最大分块大小", - "move.hint": "移动后,所选知识库/文件夹将继承新文件夹的权限设置,原先的权限设置失效。", + "move.hint": "移动后,所选知识库/文件夹将继承新文件夹的权限设置。", "noChildren": "无子目录", "noSelectedFolder": "没有选择文件夹", "noSelectedId": "没有选择 ID", diff --git a/packages/web/i18n/zh-CN/user.json b/packages/web/i18n/zh-CN/user.json index 09e5b466b7a1..62039b19be31 100644 --- a/packages/web/i18n/zh-CN/user.json +++ b/packages/web/i18n/zh-CN/user.json @@ -21,7 +21,6 @@ "delete.admin_success": "删除管理员成功", "delete.failed": "删除失败", "delete.success": "删除成功", - "has_chosen": "已选择", "login.Dingtalk": "钉钉登录", "login.error": "登录异常", "login.password_condition": "密码最多 60 位", diff --git a/packages/web/i18n/zh-Hant/account_team.json b/packages/web/i18n/zh-Hant/account_team.json index 1022a98c2f1e..b5268075b55b 100644 --- a/packages/web/i18n/zh-Hant/account_team.json +++ b/packages/web/i18n/zh-Hant/account_team.json @@ -91,7 +91,7 @@ "forbid_hint": "停用後,該邀請連結將失效。該操作不可撤銷,是否確認停用?", "forbid_success": "停用成功", "forbidden": "停用", - "link_forbidden": "禁用", + "forbidden_tip": "確認將 {{username}} 禁用?成員將被標記為“禁用”並無法登錄,不刪除操作數據,賬號下資源自動轉讓給團隊所有者。", "group": "群組", "group_name": "群組名稱", "handle_invitation": "處理團隊邀請", @@ -113,6 +113,7 @@ "label_sync": "標籤同步", "leave": "已離職", "leave_team_failed": "離開團隊異常", + "link_forbidden": "禁用", "log_admin_add_plan": "【{{name}}】將給團隊id為【{{teamId}}】的團隊添加了套餐", "log_admin_add_user": "【{{name}}】創建了一個名為【{{userName}}】的用戶", "log_admin_change_license": "【{{name}}】變更了License", @@ -196,6 +197,7 @@ "log_user": "操作人員", "login": "登入", "manage_member": "管理成員", + "manage_per": "管理權限", "member": "成員", "member_group": "所屬成員組", "move_app": "應用位置移動", @@ -222,7 +224,6 @@ "relocate_department": "部門移動", "remark": "備註", "remove_tip": "確認將 {{username}} 移出團隊?成員將被標記為“離開”,不刪除操作數據,賬號下資源自動轉讓給團隊所有者。", - "forbidden_tip": "確認將 {{username}} 禁用?成員將被標記為“禁用”並無法登錄,不刪除操作數據,賬號下資源自動轉讓給團隊所有者。", "restore_tip": "確認將 {{username}} 加入團隊嗎?\n僅恢復該成員賬號可用性及相關權限,無法恢復賬號下資源。", "restore_tip_title": "恢復確認", "retain_admin_permissions": "保留管理員權限", diff --git a/packages/web/i18n/zh-Hant/app.json b/packages/web/i18n/zh-Hant/app.json index 012e784adb0d..25b9a1c8b032 100644 --- a/packages/web/i18n/zh-Hant/app.json +++ b/packages/web/i18n/zh-Hant/app.json @@ -175,7 +175,7 @@ "module.type": "\"{{type}}\" 類型\n{{description}}", "modules.Title is required": "模組名稱不能空白", "month.unit": "號", - "move.hint": "移動後,所選應用程式/資料夾將會繼承新資料夾的權限設定,原先的權限設定將會失效。", + "move.hint": "移動後,所選應用/文件夾將繼承新文件夾的權限設置。", "move_app": "移動應用程式", "no_mcp_tools_list": "暫無數據,需先解析 MCP 地址", "node_not_intro": "這個節點沒有介紹", @@ -189,7 +189,7 @@ "pdf_enhance_parse": "PDF 增強解析", "pdf_enhance_parse_price": "{{price}}積分/頁", "pdf_enhance_parse_tips": "呼叫 PDF 識別模型進行解析,可以將其轉換成 Markdown 並保留文件中的圖片,同時也可以對掃描件進行識別,識別時間較長。", - "permission.des.manage": "在寫入權限基礎上,可以設定發布通道、檢視對話紀錄、分配這個應用程式的權限", + "permission.des.manage": "可配置發布渠道、查看日誌、分配應用權限", "permission.des.read": "可以使用這個應用程式進行對話", "permission.des.readChatLog": "可以檢視對話紀錄", "permission.des.write": "可以檢視和編輯應用程式", diff --git a/packages/web/i18n/zh-Hant/common.json b/packages/web/i18n/zh-Hant/common.json index 795af05893e1..98262cfc2bda 100644 --- a/packages/web/i18n/zh-Hant/common.json +++ b/packages/web/i18n/zh-Hant/common.json @@ -965,7 +965,7 @@ "permission.Private Tip": "僅自己可用", "permission.Public": "團隊", "permission.Public Tip": "所有團隊成員可用", - "permission.Remove InheritPermission Confirm": "此操作會導致權限繼承失效,是否繼續?", + "permission.Remove InheritPermission Confirm": "此修改與繼承權限存在衝突,會導致權限繼承失效,是否進行?", "permission.Resume InheritPermission Confirm": "要恢復繼承上層資料夾的權限嗎?", "permission.Resume InheritPermission Failed": "恢復失敗", "permission.Resume InheritPermission Success": "恢復成功", @@ -975,6 +975,7 @@ "permission.change_owner_success": "擁有權轉移成功", "permission.change_owner_tip": "轉移後您的權限將不會保留", "permission.change_owner_to": "轉移給", + "permission.common_member": "普通成員", "permission.manager": "管理員", "permission.read": "讀取權限", "permission.write": "寫入權限", diff --git a/packages/web/i18n/zh-Hant/dataset.json b/packages/web/i18n/zh-Hant/dataset.json index 0b53de95fda5..7c134ee469c9 100644 --- a/packages/web/i18n/zh-Hant/dataset.json +++ b/packages/web/i18n/zh-Hant/dataset.json @@ -133,7 +133,7 @@ "llm_paragraph_mode_force_desc": "強制使用模型自動識別段落,並忽略原文本的段落(如有)", "loading": "加載中...", "max_chunk_size": "最大分塊大小", - "move.hint": "移動後,所選資料集/資料夾將繼承新資料夾的權限設定,原先的權限設定將失效。", + "move.hint": "移動後,所選知識庫/文件夾將繼承新文件夾的權限設置。", "noChildren": "無子目錄", "noSelectedFolder": "沒有選擇文件夾", "noSelectedId": "沒有選擇 ID", diff --git a/packages/web/i18n/zh-Hant/user.json b/packages/web/i18n/zh-Hant/user.json index 0d951ee6aeb8..c08cb9a433e2 100644 --- a/packages/web/i18n/zh-Hant/user.json +++ b/packages/web/i18n/zh-Hant/user.json @@ -21,7 +21,6 @@ "delete.admin_success": "刪除管理員成功", "delete.failed": "刪除失敗", "delete.success": "刪除成功", - "has_chosen": "已選擇", "login.Dingtalk": "釘釘登入", "login.error": "登入失敗", "login.password_condition": "密碼最多可輸入 60 個字元", diff --git a/packages/web/package.json b/packages/web/package.json index 76ad3df5a802..72758b9d8e71 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -25,6 +25,7 @@ "ahooks": "^3.9.4", "date-fns": "2.30.0", "dayjs": "^1.11.7", + "next": "14.2.32", "i18next": "23.16.8", "js-cookie": "^3.0.5", "lexical": "0.12.6", diff --git a/plugins/webcrawler/SPIDER/package-lock.json b/plugins/webcrawler/SPIDER/package-lock.json index 5b2cf74ed68f..a2ca56d40f35 100644 --- a/plugins/webcrawler/SPIDER/package-lock.json +++ b/plugins/webcrawler/SPIDER/package-lock.json @@ -11,7 +11,7 @@ "dependencies": { "@types/node-fetch": "^2.6.12", "assert": "^2.1.0", - "axios": "^1.8.2", + "axios": "^1.12.1", "body-parser": "^1.20.3", "browserify-zlib": "^0.2.0", "buffer": "^6.0.3", diff --git a/plugins/webcrawler/SPIDER/package.json b/plugins/webcrawler/SPIDER/package.json index 6e2d13d21d7a..ea918ac1c4b2 100644 --- a/plugins/webcrawler/SPIDER/package.json +++ b/plugins/webcrawler/SPIDER/package.json @@ -15,7 +15,7 @@ "dependencies": { "@types/node-fetch": "^2.6.12", "assert": "^2.1.0", - "axios": "^1.8.2", + "axios": "^1.12.1", "body-parser": "^1.20.3", "browserify-zlib": "^0.2.0", "buffer": "^6.0.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1e1abf15e2c2..cc3a31fa122b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -46,7 +46,7 @@ importers: version: 10.1.4(socks@2.8.4) next-i18next: specifier: 15.4.2 - version: 15.4.2(i18next@23.16.8)(next@14.2.28(@babel/core@7.26.10)(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.85.1))(react-i18next@14.1.2(i18next@23.16.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1) + version: 15.4.2(i18next@23.16.8)(next@14.2.32(@babel/core@7.26.10)(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.85.1))(react-i18next@14.1.2(i18next@23.16.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1) prettier: specifier: 3.2.4 version: 3.2.4 @@ -75,8 +75,8 @@ importers: specifier: ^0.1.16 version: 0.1.16(@types/node@20.14.0) axios: - specifier: ^1.8.2 - version: 1.8.4 + specifier: ^1.12.1 + version: 1.12.1 cron-parser: specifier: ^4.9.0 version: 4.9.0 @@ -102,8 +102,8 @@ importers: specifier: ^5.1.3 version: 5.1.3 next: - specifier: 14.2.28 - version: 14.2.28(@babel/core@7.26.10)(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.85.1) + specifier: 14.2.32 + version: 14.2.32(@babel/core@7.26.10)(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.85.1) openai: specifier: 4.61.0 version: 4.61.0(encoding@0.1.13)(zod@3.25.51) @@ -163,8 +163,8 @@ importers: specifier: 2.4.10 version: 2.4.10 axios: - specifier: ^1.8.2 - version: 1.8.4 + specifier: ^1.12.1 + version: 1.12.1 bullmq: specifier: ^5.52.2 version: 5.52.2 @@ -232,11 +232,11 @@ importers: specifier: ^3.11.3 version: 3.13.0 next: - specifier: 14.2.28 - version: 14.2.28(@babel/core@7.26.10)(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.85.1) + specifier: 14.2.32 + version: 14.2.32(@babel/core@7.26.10)(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.85.1) nextjs-cors: specifier: ^2.2.0 - version: 2.2.0(next@14.2.28(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.85.1)) + version: 2.2.0(next@14.2.32(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.85.1)) node-cron: specifier: ^3.0.3 version: 3.0.3 @@ -318,7 +318,7 @@ importers: version: 2.1.1(@chakra-ui/system@2.6.1(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@emotion/styled@11.11.0(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(react@18.3.1))(react@18.3.1))(react@18.3.1) '@chakra-ui/next-js': specifier: 2.4.2 - version: 2.4.2(@chakra-ui/react@2.10.7(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@emotion/styled@11.11.0(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(framer-motion@9.1.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(next@14.2.28(@babel/core@7.26.10)(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.85.1))(react@18.3.1) + version: 2.4.2(@chakra-ui/react@2.10.7(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@emotion/styled@11.11.0(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(framer-motion@9.1.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(next@14.2.32(@babel/core@7.26.10)(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.85.1))(react@18.3.1) '@chakra-ui/react': specifier: 2.10.7 version: 2.10.7(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@emotion/styled@11.11.0(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(framer-motion@9.1.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -391,9 +391,12 @@ importers: lodash: specifier: ^4.17.21 version: 4.17.21 + next: + specifier: 14.2.32 + version: 14.2.32(@babel/core@7.26.10)(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.85.1) next-i18next: specifier: 15.4.2 - version: 15.4.2(i18next@23.16.8)(next@14.2.28(@babel/core@7.26.10)(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.85.1))(react-i18next@14.1.2(i18next@23.16.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1) + version: 15.4.2(i18next@23.16.8)(next@14.2.32(@babel/core@7.26.10)(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.85.1))(react-i18next@14.1.2(i18next@23.16.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1) papaparse: specifier: ^5.4.1 version: 5.4.1 @@ -457,7 +460,7 @@ importers: version: 2.1.1(@chakra-ui/system@2.6.1(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@emotion/styled@11.11.0(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(react@18.3.1))(react@18.3.1))(react@18.3.1) '@chakra-ui/next-js': specifier: 2.4.2 - version: 2.4.2(@chakra-ui/react@2.10.7(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@emotion/styled@11.11.0(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(framer-motion@9.1.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(next@14.2.28(@babel/core@7.26.10)(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.85.1))(react@18.3.1) + version: 2.4.2(@chakra-ui/react@2.10.7(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@emotion/styled@11.11.0(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(framer-motion@9.1.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(next@14.2.32(@babel/core@7.26.10)(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.85.1))(react@18.3.1) '@chakra-ui/react': specifier: 2.10.7 version: 2.10.7(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@emotion/styled@11.11.0(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(framer-motion@9.1.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -501,8 +504,8 @@ importers: specifier: ^3.7.11 version: 3.8.4(react@18.3.1) axios: - specifier: ^1.8.2 - version: 1.8.4 + specifier: ^1.12.1 + version: 1.12.1 date-fns: specifier: 2.30.0 version: 2.30.0 @@ -549,11 +552,11 @@ importers: specifier: ^5.1.3 version: 5.1.3 next: - specifier: 14.2.28 - version: 14.2.28(@babel/core@7.26.10)(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.85.1) + specifier: 14.2.32 + version: 14.2.32(@babel/core@7.26.10)(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.85.1) next-i18next: specifier: 15.4.2 - version: 15.4.2(i18next@23.16.8)(next@14.2.28(@babel/core@7.26.10)(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.85.1))(react-i18next@14.1.2(i18next@23.16.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1) + version: 15.4.2(i18next@23.16.8)(next@14.2.32(@babel/core@7.26.10)(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.85.1))(react-i18next@14.1.2(i18next@23.16.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1) nprogress: specifier: ^0.2.0 version: 0.2.0 @@ -2521,62 +2524,62 @@ packages: '@nestjs/platform-express': optional: true - '@next/env@14.2.28': - resolution: {integrity: sha512-PAmWhJfJQlP+kxZwCjrVd9QnR5x0R3u0mTXTiZDgSd4h5LdXmjxCCWbN9kq6hkZBOax8Rm3xDW5HagWyJuT37g==} + '@next/env@14.2.32': + resolution: {integrity: sha512-n9mQdigI6iZ/DF6pCTwMKeWgF2e8lg7qgt5M7HXMLtyhZYMnf/u905M18sSpPmHL9MKp9JHo56C6jrD2EvWxng==} '@next/eslint-plugin-next@14.2.26': resolution: {integrity: sha512-SPEj1O5DAVTPaWD9XPupelfT2APNIgcDYD2OzEm328BEmHaglhmYNUvxhzfJYDr12AgAfW4V3UHSV93qaeELJA==} - '@next/swc-darwin-arm64@14.2.28': - resolution: {integrity: sha512-kzGChl9setxYWpk3H6fTZXXPFFjg7urptLq5o5ZgYezCrqlemKttwMT5iFyx/p1e/JeglTwDFRtb923gTJ3R1w==} + '@next/swc-darwin-arm64@14.2.32': + resolution: {integrity: sha512-osHXveM70zC+ilfuFa/2W6a1XQxJTvEhzEycnjUaVE8kpUS09lDpiDDX2YLdyFCzoUbvbo5r0X1Kp4MllIOShw==} engines: {node: '>= 10'} cpu: [arm64] os: [darwin] - '@next/swc-darwin-x64@14.2.28': - resolution: {integrity: sha512-z6FXYHDJlFOzVEOiiJ/4NG8aLCeayZdcRSMjPDysW297Up6r22xw6Ea9AOwQqbNsth8JNgIK8EkWz2IDwaLQcw==} + '@next/swc-darwin-x64@14.2.32': + resolution: {integrity: sha512-P9NpCAJuOiaHHpqtrCNncjqtSBi1f6QUdHK/+dNabBIXB2RUFWL19TY1Hkhu74OvyNQEYEzzMJCMQk5agjw1Qg==} engines: {node: '>= 10'} cpu: [x64] os: [darwin] - '@next/swc-linux-arm64-gnu@14.2.28': - resolution: {integrity: sha512-9ARHLEQXhAilNJ7rgQX8xs9aH3yJSj888ssSjJLeldiZKR4D7N08MfMqljk77fAwZsWwsrp8ohHsMvurvv9liQ==} + '@next/swc-linux-arm64-gnu@14.2.32': + resolution: {integrity: sha512-v7JaO0oXXt6d+cFjrrKqYnR2ubrD+JYP7nQVRZgeo5uNE5hkCpWnHmXm9vy3g6foMO8SPwL0P3MPw1c+BjbAzA==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - '@next/swc-linux-arm64-musl@14.2.28': - resolution: {integrity: sha512-p6gvatI1nX41KCizEe6JkF0FS/cEEF0u23vKDpl+WhPe/fCTBeGkEBh7iW2cUM0rvquPVwPWdiUR6Ebr/kQWxQ==} + '@next/swc-linux-arm64-musl@14.2.32': + resolution: {integrity: sha512-tA6sIKShXtSJBTH88i0DRd6I9n3ZTirmwpwAqH5zdJoQF7/wlJXR8DkPmKwYl5mFWhEKr5IIa3LfpMW9RRwKmQ==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - '@next/swc-linux-x64-gnu@14.2.28': - resolution: {integrity: sha512-nsiSnz2wO6GwMAX2o0iucONlVL7dNgKUqt/mDTATGO2NY59EO/ZKnKEr80BJFhuA5UC1KZOMblJHWZoqIJddpA==} + '@next/swc-linux-x64-gnu@14.2.32': + resolution: {integrity: sha512-7S1GY4TdnlGVIdeXXKQdDkfDysoIVFMD0lJuVVMeb3eoVjrknQ0JNN7wFlhCvea0hEk0Sd4D1hedVChDKfV2jw==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - '@next/swc-linux-x64-musl@14.2.28': - resolution: {integrity: sha512-+IuGQKoI3abrXFqx7GtlvNOpeExUH1mTIqCrh1LGFf8DnlUcTmOOCApEnPJUSLrSbzOdsF2ho2KhnQoO0I1RDw==} + '@next/swc-linux-x64-musl@14.2.32': + resolution: {integrity: sha512-OHHC81P4tirVa6Awk6eCQ6RBfWl8HpFsZtfEkMpJ5GjPsJ3nhPe6wKAJUZ/piC8sszUkAgv3fLflgzPStIwfWg==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - '@next/swc-win32-arm64-msvc@14.2.28': - resolution: {integrity: sha512-l61WZ3nevt4BAnGksUVFKy2uJP5DPz2E0Ma/Oklvo3sGj9sw3q7vBWONFRgz+ICiHpW5mV+mBrkB3XEubMrKaA==} + '@next/swc-win32-arm64-msvc@14.2.32': + resolution: {integrity: sha512-rORQjXsAFeX6TLYJrCG5yoIDj+NKq31Rqwn8Wpn/bkPNy5rTHvOXkW8mLFonItS7QC6M+1JIIcLe+vOCTOYpvg==} engines: {node: '>= 10'} cpu: [arm64] os: [win32] - '@next/swc-win32-ia32-msvc@14.2.28': - resolution: {integrity: sha512-+Kcp1T3jHZnJ9v9VTJ/yf1t/xmtFAc/Sge4v7mVc1z+NYfYzisi8kJ9AsY8itbgq+WgEwMtOpiLLJsUy2qnXZw==} + '@next/swc-win32-ia32-msvc@14.2.32': + resolution: {integrity: sha512-jHUeDPVHrgFltqoAqDB6g6OStNnFxnc7Aks3p0KE0FbwAvRg6qWKYF5mSTdCTxA3axoSAUwxYdILzXJfUwlHhA==} engines: {node: '>= 10'} cpu: [ia32] os: [win32] - '@next/swc-win32-x64-msvc@14.2.28': - resolution: {integrity: sha512-1gCmpvyhz7DkB1srRItJTnmR2UwQPAUXXIg9r0/56g3O8etGmwlX68skKXJOp9EejW3hhv7nSQUJ2raFiz4MoA==} + '@next/swc-win32-x64-msvc@14.2.32': + resolution: {integrity: sha512-2N0lSoU4GjfLSO50wvKpMQgKd4HdI2UHEhQPPPnlgfBJlOgJxkjpkYBqzk08f1gItBB6xF/n+ykso2hgxuydsA==} engines: {node: '>= 10'} cpu: [x64] os: [win32] @@ -4105,8 +4108,8 @@ packages: resolution: {integrity: sha512-Xm7bpRXnDSX2YE2YFfBk2FnF0ep6tmG7xPh8iHee8MIcrgq762Nkce856dYtJYLkuIoYZvGfTs/PbZhideTcEg==} engines: {node: '>=4'} - axios@1.8.4: - resolution: {integrity: sha512-eBSYY4Y68NNlHbHBMdeDmKNtDgXWhQsJcGqzO3iLUM0GraQFSS9cVgPX5I9b3lbdFKyYoAEGAZF1DwhTaljNAw==} + axios@1.12.1: + resolution: {integrity: sha512-Kn4kbSXpkFHCGE6rBFNwIv0GQs4AvDT80jlveJDKFxjbTYMUeB4QtsdPCv6H8Cm19Je7IU6VFtRl2zWZI0rudQ==} axobject-query@4.1.0: resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==} @@ -5664,10 +5667,6 @@ packages: resolution: {integrity: sha512-yDYSgNMraqvnxiEXO4hi88+YZxaHC6QKzb5N84iRCTDeRO7ZALpir/lVmf/uXUhnwUr2O4HU8s/n6x+yNjQkHw==} engines: {node: '>= 14.17'} - form-data@4.0.2: - resolution: {integrity: sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==} - engines: {node: '>= 6'} - form-data@4.0.4: resolution: {integrity: sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==} engines: {node: '>= 6'} @@ -7444,8 +7443,8 @@ packages: react: '>= 17.0.2' react-i18next: '>= 13.5.0' - next@14.2.28: - resolution: {integrity: sha512-QLEIP/kYXynIxtcKB6vNjtWLVs3Y4Sb+EClTC/CSVzdLD1gIuItccpu/n1lhmduffI32iPGEK2cLLxxt28qgYA==} + next@14.2.32: + resolution: {integrity: sha512-fg5g0GZ7/nFc09X8wLe6pNSU8cLWbLRG3TZzPJ1BJvi2s9m7eF991se67wliM9kR5yLHRkyGKU49MMx58s3LJg==} engines: {node: '>=18.17.0'} hasBin: true peerDependencies: @@ -10796,12 +10795,12 @@ snapshots: '@chakra-ui/system': 2.6.1(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@emotion/styled@11.11.0(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(react@18.3.1))(react@18.3.1) react: 18.3.1 - '@chakra-ui/next-js@2.4.2(@chakra-ui/react@2.10.7(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@emotion/styled@11.11.0(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(framer-motion@9.1.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(next@14.2.28(@babel/core@7.26.10)(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.85.1))(react@18.3.1)': + '@chakra-ui/next-js@2.4.2(@chakra-ui/react@2.10.7(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@emotion/styled@11.11.0(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(framer-motion@9.1.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(next@14.2.32(@babel/core@7.26.10)(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.85.1))(react@18.3.1)': dependencies: '@chakra-ui/react': 2.10.7(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@emotion/styled@11.11.0(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(framer-motion@9.1.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@emotion/cache': 11.14.0 '@emotion/react': 11.11.1(@types/react@18.3.1)(react@18.3.1) - next: 14.2.28(@babel/core@7.26.10)(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.85.1) + next: 14.2.32(@babel/core@7.26.10)(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.85.1) react: 18.3.1 '@chakra-ui/object-utils@2.1.0': {} @@ -11922,37 +11921,37 @@ snapshots: '@nestjs/core': 10.4.15(@nestjs/common@10.4.16(reflect-metadata@0.2.2)(rxjs@7.8.2))(encoding@0.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.2) tslib: 2.8.1 - '@next/env@14.2.28': {} + '@next/env@14.2.32': {} '@next/eslint-plugin-next@14.2.26': dependencies: glob: 10.3.10 - '@next/swc-darwin-arm64@14.2.28': + '@next/swc-darwin-arm64@14.2.32': optional: true - '@next/swc-darwin-x64@14.2.28': + '@next/swc-darwin-x64@14.2.32': optional: true - '@next/swc-linux-arm64-gnu@14.2.28': + '@next/swc-linux-arm64-gnu@14.2.32': optional: true - '@next/swc-linux-arm64-musl@14.2.28': + '@next/swc-linux-arm64-musl@14.2.32': optional: true - '@next/swc-linux-x64-gnu@14.2.28': + '@next/swc-linux-x64-gnu@14.2.32': optional: true - '@next/swc-linux-x64-musl@14.2.28': + '@next/swc-linux-x64-musl@14.2.32': optional: true - '@next/swc-win32-arm64-msvc@14.2.28': + '@next/swc-win32-arm64-msvc@14.2.32': optional: true - '@next/swc-win32-ia32-msvc@14.2.28': + '@next/swc-win32-ia32-msvc@14.2.32': optional: true - '@next/swc-win32-x64-msvc@14.2.28': + '@next/swc-win32-x64-msvc@14.2.32': optional: true '@node-rs/jieba-android-arm-eabi@2.0.1': @@ -13699,10 +13698,10 @@ snapshots: axe-core@4.10.3: {} - axios@1.8.4: + axios@1.12.1: dependencies: follow-redirects: 1.15.9(debug@4.4.0) - form-data: 4.0.2 + form-data: 4.0.4 proxy-from-env: 1.1.0 transitivePeerDependencies: - debug @@ -15141,7 +15140,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.8.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.9.0)(eslint@8.56.0): + eslint-module-utils@2.12.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.8.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.9.0(eslint-plugin-import@2.31.0)(eslint@8.56.0))(eslint@8.56.0): dependencies: debug: 3.2.7 optionalDependencies: @@ -15152,7 +15151,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.8.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.9.0)(eslint@8.57.1): + eslint-module-utils@2.12.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.8.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.9.0(eslint-plugin-import@2.31.0)(eslint@8.57.1))(eslint@8.57.1): dependencies: debug: 3.2.7 optionalDependencies: @@ -15174,7 +15173,7 @@ snapshots: doctrine: 2.1.0 eslint: 8.56.0 eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.8.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.9.0)(eslint@8.56.0) + eslint-module-utils: 2.12.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.8.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.9.0(eslint-plugin-import@2.31.0)(eslint@8.56.0))(eslint@8.56.0) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 @@ -15203,7 +15202,7 @@ snapshots: doctrine: 2.1.0 eslint: 8.57.1 eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.8.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.9.0)(eslint@8.57.1) + eslint-module-utils: 2.12.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.8.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.9.0(eslint-plugin-import@2.31.0)(eslint@8.57.1))(eslint@8.57.1) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 @@ -15817,13 +15816,6 @@ snapshots: form-data-encoder@2.1.4: {} - form-data@4.0.2: - dependencies: - asynckit: 0.4.0 - combined-stream: 1.0.8 - es-set-tostringtag: 2.1.0 - mime-types: 2.1.35 - form-data@4.0.4: dependencies: asynckit: 0.4.0 @@ -18201,7 +18193,7 @@ snapshots: transitivePeerDependencies: - supports-color - next-i18next@15.4.2(i18next@23.16.8)(next@14.2.28(@babel/core@7.26.10)(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.85.1))(react-i18next@14.1.2(i18next@23.16.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1): + next-i18next@15.4.2(i18next@23.16.8)(next@14.2.32(@babel/core@7.26.10)(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.85.1))(react-i18next@14.1.2(i18next@23.16.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1): dependencies: '@babel/runtime': 7.26.10 '@types/hoist-non-react-statics': 3.3.6 @@ -18209,13 +18201,13 @@ snapshots: hoist-non-react-statics: 3.3.2 i18next: 23.16.8 i18next-fs-backend: 2.6.0 - next: 14.2.28(@babel/core@7.26.10)(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.85.1) + next: 14.2.32(@babel/core@7.26.10)(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.85.1) react: 18.3.1 react-i18next: 14.1.2(i18next@23.16.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - next@14.2.28(@babel/core@7.26.10)(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.85.1): + next@14.2.32(@babel/core@7.26.10)(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.85.1): dependencies: - '@next/env': 14.2.28 + '@next/env': 14.2.32 '@swc/helpers': 0.5.5 busboy: 1.6.0 caniuse-lite: 1.0.30001704 @@ -18225,25 +18217,25 @@ snapshots: react-dom: 18.3.1(react@18.3.1) styled-jsx: 5.1.1(@babel/core@7.26.10)(react@18.3.1) optionalDependencies: - '@next/swc-darwin-arm64': 14.2.28 - '@next/swc-darwin-x64': 14.2.28 - '@next/swc-linux-arm64-gnu': 14.2.28 - '@next/swc-linux-arm64-musl': 14.2.28 - '@next/swc-linux-x64-gnu': 14.2.28 - '@next/swc-linux-x64-musl': 14.2.28 - '@next/swc-win32-arm64-msvc': 14.2.28 - '@next/swc-win32-ia32-msvc': 14.2.28 - '@next/swc-win32-x64-msvc': 14.2.28 + '@next/swc-darwin-arm64': 14.2.32 + '@next/swc-darwin-x64': 14.2.32 + '@next/swc-linux-arm64-gnu': 14.2.32 + '@next/swc-linux-arm64-musl': 14.2.32 + '@next/swc-linux-x64-gnu': 14.2.32 + '@next/swc-linux-x64-musl': 14.2.32 + '@next/swc-win32-arm64-msvc': 14.2.32 + '@next/swc-win32-ia32-msvc': 14.2.32 + '@next/swc-win32-x64-msvc': 14.2.32 '@opentelemetry/api': 1.9.0 sass: 1.85.1 transitivePeerDependencies: - '@babel/core' - babel-plugin-macros - nextjs-cors@2.2.0(next@14.2.28(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.85.1)): + nextjs-cors@2.2.0(next@14.2.32(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.85.1)): dependencies: cors: 2.8.5 - next: 14.2.28(@babel/core@7.26.10)(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.85.1) + next: 14.2.32(@babel/core@7.26.10)(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.85.1) node-abi@3.74.0: dependencies: @@ -20007,7 +19999,7 @@ snapshots: terser@5.39.0: dependencies: '@jridgewell/source-map': 0.3.6 - acorn: 8.14.1 + acorn: 8.15.0 commander: 2.20.3 source-map-support: 0.5.21 diff --git a/projects/app/package.json b/projects/app/package.json index b27658c2b8af..26fe8b21e826 100644 --- a/projects/app/package.json +++ b/projects/app/package.json @@ -26,7 +26,7 @@ "@node-rs/jieba": "2.0.1", "@tanstack/react-query": "^4.24.10", "ahooks": "^3.7.11", - "axios": "^1.8.2", + "axios": "^1.12.1", "date-fns": "2.30.0", "dayjs": "^1.11.7", "echarts": "5.4.1", @@ -42,7 +42,7 @@ "lodash": "^4.17.21", "mermaid": "^10.9.4", "nanoid": "^5.1.3", - "next": "14.2.28", + "next": "14.2.32", "next-i18next": "15.4.2", "nprogress": "^0.2.0", "qrcode": "^1.5.4", diff --git a/projects/app/src/components/Layout/hooks/checkCoupon.ts b/projects/app/src/components/Layout/hooks/checkCoupon.ts index fb38039f35d1..b560da9fe175 100644 --- a/projects/app/src/components/Layout/hooks/checkCoupon.ts +++ b/projects/app/src/components/Layout/hooks/checkCoupon.ts @@ -1,22 +1,24 @@ -import { useEffect, useRef } from 'react'; +import { useEffect } from 'react'; import { getCouponCode, removeCouponCode } from '@/web/support/marketing/utils'; -import type { UserType } from '@fastgpt/global/support/user/type.d'; import { redeemCoupon } from '@/web/support/user/team/api'; +import { useUserStore } from '@/web/support/user/useUserStore'; -export const useCheckCoupon = (userInfo: UserType | null) => { - const hasCheckedCouponRef = useRef(false); +export const useCheckCoupon = () => { + const { userInfo } = useUserStore(); useEffect(() => { - if (!userInfo || hasCheckedCouponRef.current) return; + if (!userInfo) return; const couponCode = getCouponCode(); if (!couponCode) return; - hasCheckedCouponRef.current = true; - redeemCoupon(couponCode) - .catch(() => {}) - .finally(removeCouponCode); + .then(removeCouponCode) + .catch((err) => { + if (err?.message === 'Invalid coupon') { + removeCouponCode(); + } + }); }, [userInfo]); }; diff --git a/projects/app/src/components/Layout/index.tsx b/projects/app/src/components/Layout/index.tsx index 559381e96117..9d49ff1b7f28 100644 --- a/projects/app/src/components/Layout/index.tsx +++ b/projects/app/src/components/Layout/index.tsx @@ -75,7 +75,7 @@ const Layout = ({ children }: { children: JSX.Element }) => { const { userInfo, isUpdateNotification, setIsUpdateNotification } = useUserStore(); const { setUserDefaultLng } = useI18nLng(); - useCheckCoupon(userInfo); + useCheckCoupon(); const isChatPage = useMemo( () => router.pathname === '/chat' && Object.values(router.query).join('').length !== 0, diff --git a/projects/app/src/components/common/folder/SlideCard.tsx b/projects/app/src/components/common/folder/SlideCard.tsx index e4908f1dc74d..21eb39850374 100644 --- a/projects/app/src/components/common/folder/SlideCard.tsx +++ b/projects/app/src/components/common/folder/SlideCard.tsx @@ -137,7 +137,7 @@ const FolderSlideCard = ({ isInheritPermission={isInheritPermission} hasParent={hasParent} > - {({ MemberListCard, onOpenManageModal, onOpenAddMember }) => { + {({ MemberListCard, onOpenManageModal }) => { return ( <> @@ -145,26 +145,15 @@ const FolderSlideCard = ({ {t('common:permission.Collaborator')} {managePer.permission.hasManagePer && ( - - - - - - - - + + + )} - - - {t('chat:variable_invisable_in_share')} - + {chatType !== ChatTypeEnum.chat && ( + + + {t('chat:variable_invisable_in_share')} + + )} {externalVariableList.map((item) => { return ( { {/* custom variables */} {allVariableList.filter((i) => i.type === VariableInputEnum.custom).length > 0 && ( <> - - - {t('chat:variable_invisable_in_share')} - {allVariableList .filter((i) => i.type === VariableInputEnum.custom) .map((item) => ( diff --git a/projects/app/src/components/core/chat/ChatContainer/ChatBox/index.tsx b/projects/app/src/components/core/chat/ChatContainer/ChatBox/index.tsx index cc184b09579c..62e21b87ea0c 100644 --- a/projects/app/src/components/core/chat/ChatContainer/ChatBox/index.tsx +++ b/projects/app/src/components/core/chat/ChatContainer/ChatBox/index.tsx @@ -1126,7 +1126,8 @@ const ChatBox = ({ > {HomeChatRenderBox} - {allVariableList.length > 0 ? ( + {allVariableList.filter((item) => item.type !== VariableInputEnum.internal).length > + 0 ? ( diff --git a/projects/app/src/components/core/chat/ChatContainer/components/VariablePopover.tsx b/projects/app/src/components/core/chat/ChatContainer/components/VariablePopover.tsx index 8c7e03372879..c30c20d49235 100644 --- a/projects/app/src/components/core/chat/ChatContainer/components/VariablePopover.tsx +++ b/projects/app/src/components/core/chat/ChatContainer/components/VariablePopover.tsx @@ -118,19 +118,21 @@ const VariablePopover = ({ chatType }: { chatType: ChatTypeEnum }) => { {externalVariableList.length > 0 && ( - - - {t('chat:variable_invisable_in_share')} - + {chatType !== ChatTypeEnum.chat && ( + + + {t('chat:variable_invisable_in_share')} + + )} {externalVariableList.map((item) => ( - - {userSelectOptions.map((option: UserSelectOptionItemType) => { - const selected = option.value === userSelectedVal; - - return ( - - ); - })} - + + + py={3.5} + gridGap={3} + align={'center'} + list={userSelectOptions.map((option: UserSelectOptionItemType) => ({ + title: ( + + {option.value} + + ), + value: option.value + }))} + value={userSelectedVal || ''} + defaultBg={'white'} + activeBg={'white'} + onChange={(val) => onSelect(val)} + isDisabled={!!userSelectedVal} + /> + ); }); diff --git a/projects/app/src/components/support/permission/ConfigPerModal/index.tsx b/projects/app/src/components/support/permission/ConfigPerModal/index.tsx index bd5ae5434ba6..e0907cda90bc 100644 --- a/projects/app/src/components/support/permission/ConfigPerModal/index.tsx +++ b/projects/app/src/components/support/permission/ConfigPerModal/index.tsx @@ -67,7 +67,7 @@ const ConfigPerModal = ({ isInheritPermission={isInheritPermission} hasParent={hasParent} > - {({ MemberListCard, onOpenManageModal, onOpenAddMember }) => { + {({ MemberListCard, onOpenManageModal }) => { return ( <> {t('common:permission.Collaborator')} - - - - + diff --git a/projects/app/src/components/support/permission/MemberManager/ManageModal.tsx b/projects/app/src/components/support/permission/MemberManager/ManageModal.tsx deleted file mode 100644 index cf84645e7428..000000000000 --- a/projects/app/src/components/support/permission/MemberManager/ManageModal.tsx +++ /dev/null @@ -1,119 +0,0 @@ -import { useUserStore } from '@/web/support/user/useUserStore'; -import { Flex, ModalBody, Table, TableContainer, Tbody, Td, Th, Thead, Tr } from '@chakra-ui/react'; -import type { RequireOnlyOne } from '@fastgpt/global/common/type/utils'; -import { DefaultGroupName } from '@fastgpt/global/support/user/team/group/constant'; -import Avatar from '@fastgpt/web/components/common/Avatar'; -import EmptyTip from '@fastgpt/web/components/common/EmptyTip'; -import MyIcon from '@fastgpt/web/components/common/Icon'; -import Loading from '@fastgpt/web/components/common/MyLoading'; -import MyModal from '@fastgpt/web/components/common/MyModal'; -import { useRequest2 } from '@fastgpt/web/hooks/useRequest'; -import { useTranslation } from 'next-i18next'; -import React from 'react'; -import { useContextSelector } from 'use-context-selector'; -import RoleSelect from './RoleSelect'; -import RoleTags from './RoleTags'; -import { CollaboratorContext } from './context'; -export type ManageModalProps = { - onClose: () => void; -}; - -function ManageModal({ onClose }: ManageModalProps) { - const { t } = useTranslation(); - const { userInfo } = useUserStore(); - const { permission, collaboratorList, onUpdateCollaborators, onDelOneCollaborator } = - useContextSelector(CollaboratorContext, (v) => v); - - const { runAsync: onDelete, loading: isDeleting } = useRequest2(onDelOneCollaborator); - - const { runAsync: onUpdate, loading: isUpdating } = useRequest2(onUpdateCollaborators, { - successToast: t('common:update_success'), - errorToast: 'Error' - }); - - const loading = isDeleting || isUpdating; - - return ( - - - - - - - - - - - - - - {collaboratorList?.map((item) => { - return ( - - - - - - ); - })} - -
{t('user:name')}{t('user:permissions')} - {t('user:operations')} -
- - - {item.name === DefaultGroupName ? userInfo?.team.teamName : item.name} - - - - - {/* Not self; Not owner and other manager */} - {item.tmbId !== userInfo?.team?.tmbId && - (permission.isOwner || !item.permission.hasManagePer) && ( - - } - value={item.permission.role} - onChange={(permission) => { - onUpdate({ - members: item.tmbId ? [item.tmbId] : undefined, - groups: item.groupId ? [item.groupId] : undefined, - orgs: item.orgId ? [item.orgId] : undefined, - permission - }); - }} - onDelete={() => { - onDelete({ - tmbId: item.tmbId, - groupId: item.groupId, - orgId: item.orgId - } as RequireOnlyOne<{ - tmbId: string; - groupId: string; - orgId: string; - }>); - }} - /> - )} -
- {collaboratorList?.length === 0 && } -
- {loading && } -
-
- ); -} - -export default ManageModal; diff --git a/projects/app/src/components/support/permission/MemberManager/MemberItemCard.tsx b/projects/app/src/components/support/permission/MemberManager/MemberItemCard.tsx index 5811fd7a6702..4c9826114e49 100644 --- a/projects/app/src/components/support/permission/MemberManager/MemberItemCard.tsx +++ b/projects/app/src/components/support/permission/MemberManager/MemberItemCard.tsx @@ -1,92 +1,128 @@ import React from 'react'; -import { useTranslation } from 'next-i18next'; -import { Box, Checkbox, HStack, VStack } from '@chakra-ui/react'; +import { Box, Checkbox, Flex } from '@chakra-ui/react'; import Avatar from '@fastgpt/web/components/common/Avatar'; import RoleTags from './RoleTags'; import type { RoleValueType } from '@fastgpt/global/support/permission/type'; import MyIcon from '@fastgpt/web/components/common/Icon'; import OrgTags from '../../user/team/OrgTags'; -import Tag from '@fastgpt/web/components/common/Tag'; +import RoleSelect from './RoleSelect'; +import { ChevronDownIcon } from '@chakra-ui/icons'; +import { DefaultGroupName } from '@fastgpt/global/support/user/team/group/constant'; +import { useUserStore } from '@/web/support/user/useUserStore'; function MemberItemCard({ avatar, key, - onChange: _onChange, + onChange, isChecked, onDelete, name, role, orgs, - addOnly, - rightSlot + rightSlot, + onRoleChange, + disabled = false }: { avatar: string; key: string; - onChange: () => void; + onChange?: () => void; + onRoleChange?: (role: RoleValueType) => void; isChecked?: boolean; onDelete?: () => void; name: string; role?: RoleValueType; - addOnly?: boolean; orgs?: string[]; rightSlot?: React.ReactNode; + disabled?: boolean; }) { - const isAdded = addOnly && !!role; - const onChange = () => { - if (!isAdded) _onChange(); - }; - const { t } = useTranslation(); + const showRoleSelect = onRoleChange !== undefined; + const { userInfo } = useUserStore(); return ( - { + if (disabled) return; + onChange?.(); }} - onClick={onChange} > - {isChecked !== undefined && ( - - )} - - - - - {name} + + {isChecked !== undefined && ( + + )} + + + + {name === DefaultGroupName ? userInfo?.team.teamName : name} + + + {orgs && orgs.length > 0 && } + - {orgs && orgs.length > 0 && } - - {!isAdded && role && } - {isAdded && ( - - {t('user:team.collaborator.added')} - - )} - {onDelete !== undefined && ( - + {showRoleSelect && ( + + + + + +
+ } + onChange={onRoleChange} /> )} - {rightSlot} - + + {onDelete !== undefined && !disabled ? ( + { + if (disabled) return; + onDelete?.(); + }} + /> + ) : ( + + )} + + {!!rightSlot && rightSlot} + ); } diff --git a/projects/app/src/components/support/permission/MemberManager/MemberModal.tsx b/projects/app/src/components/support/permission/MemberManager/MemberModal.tsx index 3755d237e10e..514021d2e408 100644 --- a/projects/app/src/components/support/permission/MemberManager/MemberModal.tsx +++ b/projects/app/src/components/support/permission/MemberManager/MemberModal.tsx @@ -3,17 +3,13 @@ import { getTeamMembers } from '@/web/support/user/team/api'; import { getGroupList } from '@/web/support/user/team/group/api'; import useOrg from '@/web/support/user/team/org/hooks/useOrg'; import { useUserStore } from '@/web/support/user/useUserStore'; -import { ChevronDownIcon } from '@chakra-ui/icons'; -import { Box, Button, Flex, Grid, HStack, ModalBody, ModalFooter, Text } from '@chakra-ui/react'; +import { Box, Button, Flex, Grid, HStack, ModalBody, ModalFooter } from '@chakra-ui/react'; import { DEFAULT_ORG_AVATAR, DEFAULT_TEAM_AVATAR, DEFAULT_USER_AVATAR } from '@fastgpt/global/common/system/constants'; -import { type UpdateClbPermissionProps } from '@fastgpt/global/support/permission/collaborator'; -import { type MemberGroupListItemType } from '@fastgpt/global/support/permission/memberGroup/type'; import { DefaultGroupName } from '@fastgpt/global/support/user/team/group/constant'; -import { type OrgListItemType } from '@fastgpt/global/support/user/team/org/type'; import { type TeamMemberItemType } from '@fastgpt/global/support/user/team/type'; import MyAvatar from '@fastgpt/web/components/common/Avatar'; import MyIcon from '@fastgpt/web/components/common/Icon'; @@ -22,28 +18,36 @@ import MyModal from '@fastgpt/web/components/common/MyModal'; import { useRequest2 } from '@fastgpt/web/hooks/useRequest'; import { useScrollPagination } from '@fastgpt/web/hooks/useScrollPagination'; import { useTranslation } from 'next-i18next'; -import { type ValueOf } from 'next/dist/shared/lib/constants'; -import { useMemo, useRef, useState } from 'react'; +import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useContextSelector } from 'use-context-selector'; import { CollaboratorContext } from './context'; import MemberItemCard from './MemberItemCard'; -import RoleSelect from './RoleSelect'; +import type { + CollaboratorItemDetailType, + CollaboratorItemType +} from '@fastgpt/global/support/permission/collaborator'; +import type { RoleValueType } from '@fastgpt/global/support/permission/type'; +import { Permission } from '@fastgpt/global/support/permission/controller'; +import { + checkRoleUpdateConflict, + getCollaboratorId, + mergeCollaboratorList +} from '@fastgpt/global/support/permission/utils'; +import { useConfirm } from '@fastgpt/web/hooks/useConfirm'; +import { ManageRoleVal, OwnerRoleVal } from '@fastgpt/global/support/permission/constant'; +import { isObjectIdOrHexString } from 'mongoose'; const HoverBoxStyle = { bgColor: 'myGray.50', cursor: 'pointer' }; -function MemberModal({ - onClose, - addPermissionOnly: addOnly = false -}: { - onClose: () => void; - addPermissionOnly?: boolean; -}) { +function MemberModal({ onClose }: { onClose: () => void }) { const { t } = useTranslation(); const { userInfo } = useUserStore(); - const collaboratorList = useContextSelector(CollaboratorContext, (v) => v.collaboratorList); + const collaboratorDetailList = useContextSelector(CollaboratorContext, (v) => v.collaboratorList); + const isInheritPermission = useContextSelector(CollaboratorContext, (v) => v.isInheritPermission); + const defaultRole = useContextSelector(CollaboratorContext, (v) => v.defaultRole); const [filterClass, setFilterClass] = useState<'member' | 'org' | 'group'>(); const { paths, @@ -56,11 +60,7 @@ function MemberModal({ setSearchKey } = useOrg({ withPermission: false }); - const { - data: members, - ScrollData: TeamMemberScrollData, - refreshList - } = useScrollPagination(getTeamMembers, { + const { data: members, ScrollData: TeamMemberScrollData } = useScrollPagination(getTeamMembers, { pageSize: 15, params: { withPermission: true, @@ -73,11 +73,7 @@ function MemberModal({ refreshDeps: [searchKey] }); - const { - data: groups = [], - loading: loadingGroupsAndOrgs, - runAsync: refreshGroups - } = useRequest2( + const { data: groups = [], loading: loadingGroupsAndOrgs } = useRequest2( async () => { if (!userInfo?.team?.teamId) return []; return getGroupList({ @@ -91,34 +87,33 @@ function MemberModal({ } ); - const [selectedOrgList, setSelectedOrgIdList] = useState([]); + const [editCollaborators, setCollaboratorList] = useState([]); - const [selectedMemberList, setSelectedMemberList] = useState< - Omit[] - >([]); - - const [selectedGroupList, setSelectedGroupList] = useState[]>([]); - const roleList = useContextSelector(CollaboratorContext, (v) => v.roleList); - const getRoleLabelList = useContextSelector(CollaboratorContext, (v) => v.getRoleLabelList); - const [selectedRole, setSelectedRole] = useState(roleList?.read?.value); - const roleLabel = useMemo(() => { - if (selectedRole === undefined) return ''; - return getRoleLabelList(selectedRole!).join('、'); - }, [getRoleLabelList, selectedRole]); + useEffect(() => { + setCollaboratorList(collaboratorDetailList); + }, [collaboratorDetailList]); const onUpdateCollaborators = useContextSelector( CollaboratorContext, (v) => v.onUpdateCollaborators ); - const { runAsync: onConfirm, loading: isUpdating } = useRequest2( + const parentClbs = useContextSelector(CollaboratorContext, (v) => v.parentClbList); + const myRole = useContextSelector(CollaboratorContext, (v) => v.myRole); + + const { runAsync: _onConfirm, loading: isUpdating } = useRequest2( () => onUpdateCollaborators({ - members: selectedMemberList.map((item) => item.tmbId), - groups: selectedGroupList.map((item) => item._id), - orgs: selectedOrgList.map((item) => item._id), - permission: addOnly ? undefined : selectedRole! - } as UpdateClbPermissionProps>), + collaborators: editCollaborators.map( + (clb) => + ({ + tmbId: clb.tmbId, + groupId: clb.groupId, + orgId: clb.orgId, + permission: clb.permission.role + }) as CollaboratorItemType + ) + }), { successToast: t('common:add_success'), onSuccess() { @@ -127,334 +122,410 @@ function MemberModal({ } ); + const { openConfirm: openConfirmDisableInheritPer, ConfirmModal: ConfirmDisableInheritPer } = + useConfirm({ + content: t('common:permission.Remove InheritPermission Confirm') + }); + + const onConfirm = useCallback(() => { + const _parentClbs = parentClbs.map((clb) => ({ + ...clb, + permission: clb.permission.role === OwnerRoleVal ? ManageRoleVal : clb.permission.role + })); + + const newChildClbs = editCollaborators.map((clb) => ({ + ...clb, + permission: clb.permission.role + })); + + const isConflict = checkRoleUpdateConflict({ + parentClbs: _parentClbs, + newChildClbs + }); + if (isConflict && isInheritPermission) { + return openConfirmDisableInheritPer(_onConfirm)(); + } else { + return _onConfirm(); + } + }, [ + _onConfirm, + editCollaborators, + isInheritPermission, + openConfirmDisableInheritPer, + parentClbs + ]); + const entryList = useRef([ { label: t('user:team.group.members'), icon: DEFAULT_USER_AVATAR, value: 'member' }, { label: t('user:team.org.org'), icon: DEFAULT_ORG_AVATAR, value: 'org' }, { label: t('user:team.group.group'), icon: DEFAULT_TEAM_AVATAR, value: 'group' } ]); - const selectedList = useMemo(() => { - return [ - ...selectedOrgList.map((item) => ({ - id: `org-${item._id}`, - avatar: item.avatar, - name: item.name, - onDelete: () => setSelectedOrgIdList(selectedOrgList.filter((v) => v._id !== item._id)), - orgs: undefined - })), - ...selectedGroupList.map((item) => ({ - id: `group-${item._id}`, - avatar: item.avatar, - name: item.name === DefaultGroupName ? userInfo?.team.teamName : item.name, - onDelete: () => setSelectedGroupList(selectedGroupList.filter((v) => v._id !== item._id)), - orgs: undefined - })), - ...selectedMemberList.map((item) => ({ - id: `member-${item.tmbId}`, - avatar: item.avatar, - name: item.memberName, - onDelete: () => - setSelectedMemberList(selectedMemberList.filter((v) => v.tmbId !== item.tmbId)), - orgs: item.orgs - })) - ]; - }, [selectedOrgList, selectedGroupList, selectedMemberList, userInfo?.team.teamName]); + const memberWithPer = useMemo(() => { + const map = new Map(collaboratorDetailList.map((clb) => [getCollaboratorId(clb), { ...clb }])); + return members.map((member) => { + const clb = map.get(getCollaboratorId(member)); + return { + ...member, + permission: new Permission({ + role: clb?.permission.role + }) + }; + }); + }, [collaboratorDetailList, members]); + + const orgMembersWithPer = useMemo(() => { + const map = new Map(collaboratorDetailList.map((clb) => [getCollaboratorId(clb), { ...clb }])); + return orgMembers.map((member) => { + const clb = map.get(getCollaboratorId(member)); + return { + ...member, + permission: new Permission({ + role: clb?.permission.role + }) + }; + }); + }, [collaboratorDetailList, orgMembers]); return ( - - - - + + + - setSearchKey(e.target.value)} - /> + + + setSearchKey(e.target.value)} + /> + - - {/* Entry */} - {!searchKey && !filterClass && ( - <> - {entryList.current.map((item) => { - return ( - setFilterClass(item.value as any)} - > - - - {item.label} - - - - ); - })} - - )} + + {/* Entry */} + {!searchKey && !filterClass && ( + + {entryList.current.map((item) => { + return ( + setFilterClass(item.value as any)} + > + + + {item.label} + + + + ); + })} + + )} - {/* Path */} - {!searchKey && filterClass && ( - - { - if (parentId === '') { - setFilterClass(undefined); - onPathClick(''); - } else if ( - parentId === 'member' || - parentId === 'org' || - parentId === 'group' - ) { - setFilterClass(parentId); - onPathClick(''); - } else { - onPathClick(parentId); - } - }} - rootName={t('common:Team')} - /> - - )} - {(filterClass === 'member' || searchKey) && - (() => { - const Members = members?.map((member) => { - const onChange = () => { - setSelectedMemberList((state) => { - if (state.find((v) => v.tmbId === member.tmbId)) { - return state.filter((v) => v.tmbId !== member.tmbId); + {/* Path */} + {!searchKey && filterClass && ( + + { + if (parentId === '') { + setFilterClass(undefined); + onPathClick(''); + } else if ( + parentId === 'member' || + parentId === 'org' || + parentId === 'group' + ) { + setFilterClass(parentId); + onPathClick(''); + } else { + onPathClick(parentId); } - return [...state, member]; - }); - }; - const collaborator = collaboratorList?.find((v) => v.tmbId === member.tmbId); - return ( - v.tmbId === member.tmbId)} - orgs={member.orgs} + }} + rootName={t('common:Team')} + /> + + )} + {(filterClass === 'member' || searchKey) && + (() => { + const MemberList = ( + ); - }); - return searchKey ? ( - Members - ) : ( - - {Members} - - ); - })()} - {(filterClass === 'org' || searchKey) && - (() => { - const Orgs = orgs?.map((org) => { - const onChange = () => { - setSelectedOrgIdList((state) => { - if (state.find((v) => v._id === org._id)) { - return state.filter((v) => v._id !== org._id); - } - return [...state, org]; - }); - }; - const collaborator = collaboratorList?.find((v) => v.orgId === org._id); - return ( - String(v._id) === String(org._id))} - rightSlot={ - org.total && ( - { - onClickOrg(org); - // setPath(getOrgChildrenPath(org)); - e.stopPropagation(); - }} - /> - ) - } - /> + return searchKey ? ( + {MemberList} + ) : ( + + {MemberList} + ); - }); - return searchKey ? ( - Orgs - ) : ( - - {Orgs} - {orgMembers.map((member) => { - const isChecked = !!selectedMemberList.find( - (v) => v.tmbId === member.tmbId - ); - const collaborator = collaboratorList?.find( - (v) => v.tmbId === member.tmbId - ); - return ( - { - setSelectedMemberList((state) => { - if (state.find((v) => v.tmbId === member.tmbId)) { - return state.filter((v) => v.tmbId !== member.tmbId); - } - return [...state, member]; - }); - }} - isChecked={isChecked} - role={collaborator?.permission.role} - addOnly={addOnly && !!member.permission.role} - orgs={member.orgs} - /> - ); - })} - - ); - })()} - {(filterClass === 'group' || searchKey) && - groups?.map((group) => { - const onChange = () => { - setSelectedGroupList((state) => { - if (state.find((v) => v._id === group._id)) { - return state.filter((v) => v._id !== group._id); - } - return [...state, group]; + })()} + {(filterClass === 'org' || searchKey) && + (() => { + const Orgs = orgs?.map((org) => { + const addTheOrg = () => { + setCollaboratorList((state) => { + if (state.find((v) => v.orgId === org._id)) { + return state.filter((v) => v.orgId !== org._id); + } + return [ + ...state, + { + ...org, + orgId: org._id, + permission: new Permission({ role: defaultRole }) + } + ]; + }); + }; + const isChecked = !!editCollaborators.find((v) => v.orgId === org._id); + return ( + { + onClickOrg(org); + e.stopPropagation(); + }} + /> + ) + } + /> + ); + }); + return searchKey ? ( + {Orgs} + ) : ( + + {Orgs} + + + ); + })()} + {(filterClass === 'group' || searchKey) && ( + + {groups?.map((group) => { + const addGroup = () => { + setCollaboratorList((state) => { + if (state.find((v) => v.groupId === group._id)) { + return state.filter((v) => v.groupId !== group._id); + } + return [ + ...state, + { + ...group, + groupId: group._id, + permission: new Permission({ role: defaultRole }) + } + ]; + }); + }; + const isChecked = !!editCollaborators.find((v) => v.groupId === group._id); + return ( + + ); + })} + + )} + + + + + {`${t('common:chosen')}: ${editCollaborators.length}`} + + {editCollaborators.map((clb) => { + const onDelete = () => { + setCollaboratorList((state) => { + return state.filter((v) => getCollaboratorId(v) !== getCollaboratorId(clb)); + }); + }; + const onRoleChange = (role: RoleValueType) => { + setCollaboratorList((state) => { + const index = state.findIndex( + (v) => getCollaboratorId(v) === getCollaboratorId(clb) + ); + if (index === -1) return state; + return [ + ...state.slice(0, index), + { + ...state[index], + permission: new Permission({ role }) + }, + ...state.slice(index + 1) + ]; }); }; - const collaborator = collaboratorList?.find((v) => v.groupId === group._id); return ( v._id === group._id)} - addOnly={addOnly} /> ); })} - - - - - - {`${t('user:has_chosen')}: `} - {selectedMemberList.length + selectedGroupList.length + selectedOrgList.length} - - - {selectedList.map((item) => { - return ( - - ); - })} - - - - - - {!addOnly && !!roleList && ( - - {roleLabel} - - } - onChange={(v) => setSelectedRole(v)} - /> - )} - {addOnly && ( - - - {t('user:permission_add_tip')} - - )} - - - + + + + + + + + + ); } export default MemberModal; + +const RenderMemberList = ({ + members, + setCollaboratorList, + editCollaborators, + defaultRole +}: { + members: Array & { permission: Permission }>; + setCollaboratorList: React.Dispatch>; + editCollaborators: CollaboratorItemDetailType[]; + defaultRole: RoleValueType; +}) => { + const { userInfo } = useUserStore(); + const myRole = useContextSelector(CollaboratorContext, (v) => v.myRole); + + return ( + <> + {members?.map((member) => { + const addTheMember = () => { + setCollaboratorList((state) => { + if (state.find((v) => v.tmbId === member.tmbId)) { + return state.filter((v) => v.tmbId !== member.tmbId); + } + return [ + ...state, + { + tmbId: member.tmbId, + avatar: member.avatar, + name: member.memberName, + teamId: member.teamId, + permission: new Permission({ role: defaultRole }) + } + ]; + }); + }; + const isChecked = !!editCollaborators.find((v) => v.tmbId === member.tmbId); + return ( + + ); + })} + + ); +}; diff --git a/projects/app/src/components/support/permission/MemberManager/RoleSelect.tsx b/projects/app/src/components/support/permission/MemberManager/RoleSelect.tsx index 6d4e29700c75..150eea3f30aa 100644 --- a/projects/app/src/components/support/permission/MemberManager/RoleSelect.tsx +++ b/projects/app/src/components/support/permission/MemberManager/RoleSelect.tsx @@ -18,6 +18,7 @@ import { Permission } from '@fastgpt/global/support/permission/controller'; import { CollaboratorContext } from './context'; import { useTranslation } from 'next-i18next'; import MyDivider from '@fastgpt/web/components/common/MyDivider'; +import { ManageRoleVal } from '@fastgpt/global/support/permission/constant'; export type PermissionSelectProps = { value?: RoleValueType; @@ -47,16 +48,15 @@ function RoleSelect({ offset = [0, 5], Button, width = 'auto', - onDelete + onDelete, + disabled }: PermissionSelectProps) { const { t } = useTranslation(); const ref = useRef(null); const closeTimer = useRef(); - const { permission, roleList: permissionList } = useContextSelector( - CollaboratorContext, - (v) => v - ); + const { roleList: permissionList } = useContextSelector(CollaboratorContext, (v) => v); + const myRole = useContextSelector(CollaboratorContext, (v) => v.myRole); const [isOpen, setIsOpen] = useState(false); @@ -72,15 +72,18 @@ function RoleSelect({ }; }); + const singleOptions = list.filter((item) => item.checkBoxType === 'single'); + const per = new Permission({ role }); + return { - singleOptions: list.filter( - (item) => - item.checkBoxType === 'single' && - (permission.isOwner || item.value !== permissionList['manage'].value) - ), + singleOptions: myRole.isOwner + ? singleOptions + : myRole.hasManagePer && !per.hasManagePer + ? singleOptions.filter((item) => item.value !== ManageRoleVal) + : [], checkboxList: list.filter((item) => item.checkBoxType === 'multiple') }; - }, [permission.isOwner, permissionList]); + }, [myRole.hasManagePer, myRole.isOwner, permissionList, role]); const selectedSingleValue = useMemo(() => { if (!permissionList) return undefined; @@ -120,6 +123,7 @@ function RoleSelect({ ref={ref} w="fit-content" onMouseEnter={() => { + if (disabled) return; if (trigger === 'hover') { setIsOpen(true); } @@ -135,8 +139,10 @@ function RoleSelect({ > { if (trigger === 'click') { + if (disabled) return; setIsOpen(!isOpen); } }} @@ -158,10 +164,10 @@ function RoleSelect({ {/* The list of single select permissions */} {roleOptions.singleOptions.map((item) => { const change = () => { - const per = new Permission({ role }); - per.removeRole(selectedSingleValue); - per.addRole(item.value); - onSelectRole(per.role); + if (disabled) { + return; + } + onSelectRole(item.value); }; return ( @@ -187,7 +193,7 @@ function RoleSelect({ ); })} - {roleOptions.checkboxList.length > 0 && ( + {roleOptions.checkboxList.length > 0 && roleOptions.singleOptions.length > 0 && ( <> @@ -197,7 +203,8 @@ function RoleSelect({ )} {roleOptions.checkboxList.map((item) => { - const change = () => { + const change = (e: React.MouseEvent) => { + if ((e.target as HTMLElement).tagName === 'INPUT') return; const per = new Permission({ role }); if (per.checkRole(item.value)) { per.removeRole(item.value); @@ -216,9 +223,13 @@ function RoleSelect({ } : {})} {...MenuStyle} + onClick={(e) => { + if (disabled) return; + change(e); + }} > - - + + {t(item.name as any)} {t(item.description as any)} diff --git a/projects/app/src/components/support/permission/MemberManager/RoleTags.tsx b/projects/app/src/components/support/permission/MemberManager/RoleTags.tsx index fd50190ee54b..6396d0b5da3f 100644 --- a/projects/app/src/components/support/permission/MemberManager/RoleTags.tsx +++ b/projects/app/src/components/support/permission/MemberManager/RoleTags.tsx @@ -19,16 +19,16 @@ function RoleTags({ permission }: PermissionTagsProp) { const roleTagList = getRoleLabelList(permission); return ( - + {roleTagList.map((item) => ( {t(item as any)} diff --git a/projects/app/src/components/support/permission/MemberManager/context.tsx b/projects/app/src/components/support/permission/MemberManager/context.tsx index 18b3aadc5a28..5dd70f34158b 100644 --- a/projects/app/src/components/support/permission/MemberManager/context.tsx +++ b/projects/app/src/components/support/permission/MemberManager/context.tsx @@ -1,6 +1,7 @@ import { useDisclosure } from '@chakra-ui/react'; import type { - CollaboratorItemType, + CollaboratorItemDetailType, + CollaboratorListType, UpdateClbPermissionProps } from '@fastgpt/global/support/permission/collaborator'; import { Permission } from '@fastgpt/global/support/permission/controller'; @@ -9,7 +10,7 @@ import type { RoleListType, RoleValueType } from '@fastgpt/global/support/permission/type'; -import { type ReactNode, useCallback } from 'react'; +import { type ReactNode, useCallback, useMemo } from 'react'; import { createContext } from 'use-context-selector'; import dynamic from 'next/dynamic'; @@ -19,14 +20,15 @@ import { useSystemStore } from '@/web/common/system/useSystemStore'; import { useConfirm } from '@fastgpt/web/hooks/useConfirm'; import type { RequireOnlyOne } from '@fastgpt/global/common/type/utils'; import { useTranslation } from 'next-i18next'; -import { CommonRoleList } from '@fastgpt/global/support/permission/constant'; +import { CommonRoleList, NullRoleVal } from '@fastgpt/global/support/permission/constant'; +import { useUserStore } from '@/web/support/user/useUserStore'; const MemberModal = dynamic(() => import('./MemberModal')); -const ManageModal = dynamic(() => import('./ManageModal')); export type MemberManagerInputPropsType = { permission: Permission; - onGetCollaboratorList: () => Promise; + defaultRole: RoleValueType; + onGetCollaboratorList: () => Promise; roleList?: RoleListType; onUpdateCollaborators: (props: UpdateClbPermissionProps) => Promise; onDelOneCollaborator: ( @@ -35,22 +37,26 @@ export type MemberManagerInputPropsType = { refreshDeps?: any[]; }; -export type MemberManagerPropsType = MemberManagerInputPropsType & { - collaboratorList: CollaboratorItemType[]; +export type CollaboratorContextType = MemberManagerInputPropsType & { + collaboratorList: CollaboratorItemDetailType[]; + parentClbList: CollaboratorItemDetailType[]; + myRole: Permission; refetchCollaboratorList: () => void; isFetchingCollaborator: boolean; getRoleLabelList: (role: RoleValueType) => string[]; + isInheritPermission?: boolean; }; + export type ChildrenProps = { - onOpenAddMember: () => void; onOpenManageModal: () => void; MemberListCard: (props: MemberListCardProps) => JSX.Element; }; -type CollaboratorContextType = MemberManagerPropsType & {}; - export const CollaboratorContext = createContext({ + myRole: new Permission(), + defaultRole: NullRoleVal, collaboratorList: [], + parentClbList: [], roleList: CommonRoleList, onUpdateCollaborators: () => { throw new Error('Function not implemented.'); @@ -64,11 +70,12 @@ export const CollaboratorContext = createContext({ refetchCollaboratorList: (): void => { throw new Error('Function not implemented.'); }, - onGetCollaboratorList: (): Promise => { + onGetCollaboratorList: (): Promise => { throw new Error('Function not implemented.'); }, isFetchingCollaborator: false, - permission: new Permission() + permission: new Permission(), + isInheritPermission: false }); const CollaboratorContextProvider = ({ @@ -80,9 +87,8 @@ const CollaboratorContextProvider = ({ children, refetchResource, refreshDeps = [], - isInheritPermission, - hasParent, - addPermissionOnly + defaultRole, + isInheritPermission }: MemberManagerInputPropsType & { children: (props: ChildrenProps) => ReactNode; refetchResource?: () => void; @@ -105,23 +111,31 @@ const CollaboratorContextProvider = ({ const { feConfigs } = useSystemStore(); const { - data: collaboratorList = [], + data: { clbs: collaboratorList = [], parentClbs: parentClbList = [] } = { + clbs: [], + parentClbs: [] + }, runAsync: refetchCollaboratorList, loading: isFetchingCollaborator } = useRequest2( async () => { if (feConfigs.isPlus) { - const data = await onGetCollaboratorList(); - return data.map((item) => { - return { - ...item, - permission: new Permission({ - role: item.permission.role - }) - }; - }); + const { clbs, parentClbs = [] } = await onGetCollaboratorList(); + return { + clbs: clbs.map((clb) => ({ + ...clb, + permission: new Permission({ role: clb.permission.role }) + })), + parentClbs: parentClbs.map((clb) => ({ + ...clb, + permission: new Permission({ role: clb.permission.role }) + })) + }; } - return []; + return { + clbs: [], + parentClbs: [] + }; }, { manual: false, @@ -160,18 +174,22 @@ const CollaboratorContextProvider = ({ [roleList] ); - const { ConfirmModal, openConfirm } = useConfirm({}); - const { - isOpen: isOpenAddMember, - onOpen: onOpenAddMember, - onClose: onCloseAddMember - } = useDisclosure(); const { isOpen: isOpenManageModal, onOpen: onOpenManageModal, onClose: onCloseManageModal } = useDisclosure(); + const { userInfo } = useUserStore(); + const myRole = useMemo(() => { + return ( + collaboratorList.find((v) => v.tmbId === userInfo?.team?.tmbId)?.permission ?? + new Permission({ + isOwner: userInfo?.team.permission.isOwner + }) + ); + }, [collaboratorList, userInfo?.team.permission.isOwner, userInfo?.team?.tmbId]); + const contextValue = { permission, onGetCollaboratorList, @@ -181,60 +199,27 @@ const CollaboratorContextProvider = ({ roleList, onUpdateCollaborators: onUpdateCollaboratorsThen, onDelOneCollaborator: onDelOneCollaboratorThen, - getRoleLabelList + getRoleLabelList, + defaultRole, + parentClbList, + myRole, + isInheritPermission }; - const onOpenAddMemberModal = () => { - if (isInheritPermission && hasParent) { - openConfirm( - () => { - onOpenAddMember(); - }, - undefined, - t('common:permission.Remove InheritPermission Confirm') - )(); - } else { - onOpenAddMember(); - } - }; - const onOpenManageModalModal = () => { - if (isInheritPermission && hasParent) { - openConfirm( - () => { - onOpenManageModal(); - }, - undefined, - t('common:permission.Remove InheritPermission Confirm') - )(); - } else { - onOpenManageModal(); - } - }; return ( {children({ - onOpenAddMember: onOpenAddMemberModal, - onOpenManageModal: onOpenManageModalModal, + onOpenManageModal, MemberListCard })} - {isOpenAddMember && ( - { - onCloseAddMember(); - refetchResource?.(); - }} - addPermissionOnly={addPermissionOnly} - /> - )} {isOpenManageModal && ( - { onCloseManageModal(); refetchResource?.(); }} /> )} - ); }; diff --git a/projects/app/src/components/support/user/team/OrgTags/index.tsx b/projects/app/src/components/support/user/team/OrgTags/index.tsx index cb2ea00ae6bc..7ecf3c082a7c 100644 --- a/projects/app/src/components/support/user/team/OrgTags/index.tsx +++ b/projects/app/src/components/support/user/team/OrgTags/index.tsx @@ -26,10 +26,10 @@ function OrgTags({ orgs, type = 'simple' }: { orgs?: string[]; type?: 'simple' | > {type === 'simple' ? ( diff --git a/projects/app/src/global/core/workflow/api.d.ts b/projects/app/src/global/core/workflow/api.d.ts index e638aea48996..d81f06b98b44 100644 --- a/projects/app/src/global/core/workflow/api.d.ts +++ b/projects/app/src/global/core/workflow/api.d.ts @@ -1,4 +1,4 @@ -import { AppSchema } from '@fastgpt/global/core/app/type'; +import type { AppSchema } from '@fastgpt/global/core/app/type'; import type { ChatHistoryItemResType } from '@fastgpt/global/core/chat/type'; import type { RuntimeNodeItemType } from '@fastgpt/global/core/workflow/runtime/type'; import type { WorkflowInteractiveResponseType } from '@fastgpt/global/core/workflow/template/system/interactive/type'; @@ -15,6 +15,7 @@ export type PostWorkflowDebugProps = { appId: string; query?: UserChatItemValueItemType[]; history?: ChatItemType[]; + chatConfig?: AppSchema['chatConfig']; }; export type PostWorkflowDebugResponse = WorkflowDebugResponse & { diff --git a/projects/app/src/pageComponents/account/bill/BillTable.tsx b/projects/app/src/pageComponents/account/bill/BillTable.tsx index bdb78f0d9ee0..b12c736666f7 100644 --- a/projects/app/src/pageComponents/account/bill/BillTable.tsx +++ b/projects/app/src/pageComponents/account/bill/BillTable.tsx @@ -64,10 +64,11 @@ const BillTable = () => { pageSize } = usePagination(getBills, { defaultPageSize: 20, + storeToQuery: true, params: { type: billType }, - defaultRequest: false + refreshDeps: [billType] }); const { runAsync: handleRefreshPayOrder, loading: isRefreshing } = useRequest2( @@ -91,10 +92,6 @@ const BillTable = () => { } ); - useEffect(() => { - getData(1); - }, [billType]); - return ( diff --git a/projects/app/src/pageComponents/account/team/Audit/index.tsx b/projects/app/src/pageComponents/account/team/Audit/index.tsx index 3174790ca9b9..8870552ff01d 100644 --- a/projects/app/src/pageComponents/account/team/Audit/index.tsx +++ b/projects/app/src/pageComponents/account/team/Audit/index.tsx @@ -72,7 +72,7 @@ function AuditLog({ Tabs }: { Tabs: React.ReactNode }) { isLoading: loadingLogs, ScrollData: LogScrollData } = useScrollPagination(getOperationLogs, { - pageSize: 20, + pageSize: 30, refreshDeps: [searchParams], params: searchParams }); diff --git a/projects/app/src/pageComponents/account/team/GroupManage/GroupManageMember.tsx b/projects/app/src/pageComponents/account/team/GroupManage/GroupManageMember.tsx index 83e19311576d..76b5ddf9488b 100644 --- a/projects/app/src/pageComponents/account/team/GroupManage/GroupManageMember.tsx +++ b/projects/app/src/pageComponents/account/team/GroupManage/GroupManageMember.tsx @@ -216,8 +216,10 @@ function GroupEditModal({ - {t('common:chosen') + ': ' + selected.length} - + + {t('common:chosen') + ': ' + selected.length} + + {selected.map((member) => { return ( - - {`${t('common:chosen')}:${selected.length}`} + + {`${t('common:chosen')}:${selected.length}`} {selected.map((member) => { return ( state.collaboratorList ); - const onUpdateCollaborators = useContextSelector( - CollaboratorContext, - (state) => state.onUpdateCollaborators - ); const onDelOneCollaborator = useContextSelector( CollaboratorContext, (state) => state.onDelOneCollaborator ); + const refetchCollaborators = useContextSelector( + CollaboratorContext, + (state) => state.refetchCollaboratorList + ); const [isExpandMember, setExpandMember] = useToggle(true); const [isExpandGroup, setExpandGroup] = useToggle(true); @@ -127,12 +129,15 @@ function PermissionManage({ permission.removeRole(per); } - return onUpdateCollaborators({ - ...(clb.tmbId && { members: [clb.tmbId] }), - ...(clb.groupId && { groups: [clb.groupId] }), - ...(clb.orgId && { orgs: [clb.orgId] }), + return updateOneMemberPermission({ + tmbId: clb.tmbId, + groupId: clb.groupId, + orgId: clb.orgId, permission: permission.role }); + }, + { + onSuccess: refetchCollaborators } ); @@ -200,10 +205,9 @@ function PermissionManage({ size="md" borderRadius={'md'} ml={3} - leftIcon={} onClick={onOpenAddMember} > - {t('user:permission.Add')} + {t('account_team:manage_per')} )} @@ -268,25 +272,25 @@ function PermissionManage({ { return userInfo?.team ? ( { refreshDeps={[userInfo?.team.teamId]} addPermissionOnly={true} > - {({ onOpenAddMember }) => } + {({ onOpenManageModal }) => ( + + )} ) : null; }; diff --git a/projects/app/src/pageComponents/app/detail/InfoModal.tsx b/projects/app/src/pageComponents/app/detail/InfoModal.tsx index c1ca2cd69017..b5e5dbca34d1 100644 --- a/projects/app/src/pageComponents/app/detail/InfoModal.tsx +++ b/projects/app/src/pageComponents/app/detail/InfoModal.tsx @@ -31,6 +31,7 @@ import { useTranslation } from 'next-i18next'; import React, { useCallback } from 'react'; import { useForm } from 'react-hook-form'; import { useContextSelector } from 'use-context-selector'; +import { ReadRoleVal } from '@fastgpt/global/support/permission/constant'; const InfoModal = ({ onClose }: { onClose: () => void }) => { const { t } = useTranslation(); @@ -100,25 +101,6 @@ const InfoModal = ({ onClose }: { onClose: () => void }) => { [handleSubmit, onClose, saveSubmitError, saveSubmitSuccess] ); - const onUpdateCollaborators = ({ - members, - groups, - orgs, - permission - }: { - members?: string[]; - groups?: string[]; - orgs?: string[]; - permission: PermissionValueType; - }) => - postUpdateAppCollaborators({ - members, - groups, - permission, - orgs, - appId: appDetail._id - }); - const onDelCollaborator = async ( props: RequireOnlyOne<{ tmbId: string; groupId: string; orgId: string }> ) => @@ -185,15 +167,14 @@ const InfoModal = ({ onClose }: { onClose: () => void }) => { )} getCollaboratorList(appDetail._id)} roleList={AppRoleList} - onUpdateCollaborators={async (props) => - onUpdateCollaborators({ - permission: props.permission, - members: props.members, - groups: props.groups, - orgs: props.orgs + onUpdateCollaborators={async ({ collaborators }) => + postUpdateAppCollaborators({ + collaborators, + appId: appDetail._id }) } onDelOneCollaborator={onDelCollaborator} @@ -201,7 +182,7 @@ const InfoModal = ({ onClose }: { onClose: () => void }) => { isInheritPermission={appDetail.inheritPermission} hasParent={!!appDetail.parentId} > - {({ MemberListCard, onOpenManageModal, onOpenAddMember }) => { + {({ MemberListCard, onOpenManageModal }) => { return ( <> void }) => { w="full" > {t('common:permission.Collaborator')} - - - - + diff --git a/projects/app/src/pageComponents/app/detail/MCPTools/Header.tsx b/projects/app/src/pageComponents/app/detail/MCPTools/Header.tsx index fa944f022007..cf7ad7fd4f5c 100644 --- a/projects/app/src/pageComponents/app/detail/MCPTools/Header.tsx +++ b/projects/app/src/pageComponents/app/detail/MCPTools/Header.tsx @@ -67,7 +67,7 @@ const Header = ({ diff --git a/projects/app/src/pageComponents/app/detail/Publish/index.tsx b/projects/app/src/pageComponents/app/detail/Publish/index.tsx index 43980ef4255f..72591457ba21 100644 --- a/projects/app/src/pageComponents/app/detail/Publish/index.tsx +++ b/projects/app/src/pageComponents/app/detail/Publish/index.tsx @@ -17,7 +17,7 @@ const Link = dynamic(() => import('./Link')); const API = dynamic(() => import('./API')); const FeiShu = dynamic(() => import('./FeiShu')); const DingTalk = dynamic(() => import('./DingTalk')); -// const Wecom = dynamic(() => import('./Wecom')); +const Wecom = dynamic(() => import('./Wecom')); const OffiAccount = dynamic(() => import('./OffiAccount')); const OutLink = () => { @@ -64,13 +64,18 @@ const OutLink = () => { } ] : []), - // { - // icon: 'core/app/publish/wecom', - // title: t('publish:wecom.bot'), - // desc: t('publish:wecom.bot_desc'), - // value: PublishChannelEnum.wecom, - // isProFn: true - // }, + + ...(feConfigs?.show_publish_wecom !== false + ? [ + { + icon: 'core/app/publish/wecom', + title: t('publish:wecom.bot'), + desc: t('publish:wecom.bot_desc'), + value: PublishChannelEnum.wecom, + isProFn: true + } + ] + : []), ...(feConfigs?.show_publish_offiaccount !== false ? [ { @@ -135,7 +140,7 @@ const OutLink = () => { {linkType === PublishChannelEnum.apikey && } {linkType === PublishChannelEnum.feishu && } {linkType === PublishChannelEnum.dingtalk && } - {/* {linkType === PublishChannelEnum.wecom && } */} + {linkType === PublishChannelEnum.wecom && } {linkType === PublishChannelEnum.officialAccount && } diff --git a/projects/app/src/pageComponents/app/detail/SimpleApp/Header.tsx b/projects/app/src/pageComponents/app/detail/SimpleApp/Header.tsx index 26d0c436ff52..18621afbfad2 100644 --- a/projects/app/src/pageComponents/app/detail/SimpleApp/Header.tsx +++ b/projects/app/src/pageComponents/app/detail/SimpleApp/Header.tsx @@ -195,7 +195,7 @@ const Header = ({ { const onCheckRunError = useCallback((e: FieldErrors>) => { const hasRequiredNodeVar = - e.nodeVariables && Object.values(e.nodeVariables).some((item) => item.type === 'required'); + e.nodeVariables && Object.values(e.nodeVariables).some((item) => item.type === 'validate'); if (hasRequiredNodeVar) { return setCurrentTab(TabEnum.node); } const hasRequiredGlobalVar = - e.variables && Object.values(e.variables).some((item) => item.type === 'required'); + e.variables && Object.values(e.variables).some((item) => item.type === 'validate'); if (hasRequiredGlobalVar) { setCurrentTab(TabEnum.global); diff --git a/projects/app/src/pageComponents/app/detail/WorkflowComponents/Flow/nodes/render/NodeCard.tsx b/projects/app/src/pageComponents/app/detail/WorkflowComponents/Flow/nodes/render/NodeCard.tsx index 5db5fe7719b0..39cb937c90b2 100644 --- a/projects/app/src/pageComponents/app/detail/WorkflowComponents/Flow/nodes/render/NodeCard.tsx +++ b/projects/app/src/pageComponents/app/detail/WorkflowComponents/Flow/nodes/render/NodeCard.tsx @@ -2,7 +2,10 @@ import React, { useCallback, useMemo } from 'react'; import { Box, Button, Flex, useDisclosure, type FlexProps } from '@chakra-ui/react'; import MyIcon from '@fastgpt/web/components/common/Icon'; import Avatar from '@fastgpt/web/components/common/Avatar'; -import type { FlowNodeItemType } from '@fastgpt/global/core/workflow/type/node.d'; +import type { + FlowNodeItemType, + StoreNodeItemType +} from '@fastgpt/global/core/workflow/type/node.d'; import { useTranslation } from 'next-i18next'; import { useEditTitle } from '@/web/common/hooks/useEditTitle'; import { useToast } from '@fastgpt/web/hooks/useToast'; @@ -458,7 +461,9 @@ const MenuRender = React.memo(function MenuRender({ setNodes((state) => { const node = state.find((node) => node.id === nodeId); if (!node) return state; - const template = { + const template: Omit = { + flowNodeType: node.data.flowNodeType, + parentNodeId: node.data.parentNodeId, avatar: node.data.avatar, name: computedNewNodeName({ templateName: node.data.name, @@ -466,15 +471,27 @@ const MenuRender = React.memo(function MenuRender({ pluginId: node.data.pluginId }), intro: node.data.intro, - flowNodeType: node.data.flowNodeType, - inputs: node.data.inputs, - outputs: node.data.outputs, + toolDescription: node.data.toolDescription, showStatus: node.data.showStatus, - pluginId: node.data.pluginId, + version: node.data.version, versionLabel: node.data.versionLabel, isLatestVersion: node.data.isLatestVersion, - toolConfig: node.data.toolConfig + + catchError: node.data.catchError, + inputs: node.data.inputs, + outputs: node.data.outputs, + + pluginId: node.data.pluginId, + isFolder: node.data.isFolder, + pluginData: node.data.pluginData, + + toolConfig: node.data.toolConfig, + + currentCost: node.data.currentCost, + systemKeyCost: node.data.systemKeyCost, + hasTokenFee: node.data.hasTokenFee, + hasSystemSecret: node.data.hasSystemSecret }; return [ diff --git a/projects/app/src/pageComponents/app/detail/WorkflowComponents/context/index.tsx b/projects/app/src/pageComponents/app/detail/WorkflowComponents/context/index.tsx index 62c4687a7bc1..c357e097ab9e 100644 --- a/projects/app/src/pageComponents/app/detail/WorkflowComponents/context/index.tsx +++ b/projects/app/src/pageComponents/app/detail/WorkflowComponents/context/index.tsx @@ -719,7 +719,8 @@ const WorkflowContextProvider = ({ }, query: debugData.query, // 添加 query 参数 history: debugData.history, - appId + appId, + chatConfig: appDetail.chatConfig }); // 4. Store debug result diff --git a/projects/app/src/pageComponents/dashboard/apps/List.tsx b/projects/app/src/pageComponents/dashboard/apps/List.tsx index 43b9cc32351a..be1b3d428ef6 100644 --- a/projects/app/src/pageComponents/dashboard/apps/List.tsx +++ b/projects/app/src/pageComponents/dashboard/apps/List.tsx @@ -17,7 +17,7 @@ import { useFolderDrag } from '@/components/common/folder/useFolderDrag'; import dynamic from 'next/dynamic'; import type { EditResourceInfoFormType } from '@/components/common/Modal/EditResourceModal'; import MyMenu, { type MenuItemType } from '@fastgpt/web/components/common/MyMenu'; -import { AppRoleList } from '@fastgpt/global/support/permission/app/constant'; +import { AppDefaultRoleVal, AppRoleList } from '@fastgpt/global/support/permission/app/constant'; import { deleteAppCollaborators, getCollaboratorList, @@ -36,8 +36,8 @@ import { useSystem } from '@fastgpt/web/hooks/useSystem'; import { useChatStore } from '@/web/core/chat/context/useChatStore'; import { type RequireOnlyOne } from '@fastgpt/global/common/type/utils'; import UserBox from '@fastgpt/web/components/common/UserBox'; -import { type PermissionValueType } from '@fastgpt/global/support/permission/type'; import { ChatSidebarPaneEnum } from '@/pageComponents/chat/constants'; +import { ReadRoleVal } from '@fastgpt/global/support/permission/constant'; const HttpEditModal = dynamic(() => import('./HttpPluginEditModal')); const ListItem = () => { @@ -435,15 +435,11 @@ const ListItem = () => { avatar={editPerApp.avatar} name={editPerApp.name} managePer={{ + defaultRole: ReadRoleVal, permission: editPerApp.permission, onGetCollaboratorList: () => getCollaboratorList(editPerApp._id), roleList: AppRoleList, - onUpdateCollaborators: (props: { - members?: string[]; - groups?: string[]; - orgs?: string[]; - permission: PermissionValueType; - }) => + onUpdateCollaborators: (props) => postUpdateAppCollaborators({ ...props, appId: editPerApp._id diff --git a/projects/app/src/pageComponents/dataset/MemberManager.tsx b/projects/app/src/pageComponents/dataset/MemberManager.tsx index e2e367bfe4e5..3f0a7f3f02d0 100644 --- a/projects/app/src/pageComponents/dataset/MemberManager.tsx +++ b/projects/app/src/pageComponents/dataset/MemberManager.tsx @@ -11,7 +11,7 @@ function MemberManager({ managePer }: { managePer: MemberManagerInputPropsType } return ( - {({ MemberListCard, onOpenManageModal, onOpenAddMember }) => { + {({ MemberListCard, onOpenManageModal }) => { return ( <> @@ -30,17 +30,6 @@ function MemberManager({ managePer }: { managePer: MemberManagerInputPropsType } _hover={{ color: 'primary.500' }} /> - - - diff --git a/projects/app/src/pageComponents/dataset/detail/CollectionCard/Context.tsx b/projects/app/src/pageComponents/dataset/detail/CollectionCard/Context.tsx index 1e8d95ac3f38..b49e76c3095f 100644 --- a/projects/app/src/pageComponents/dataset/detail/CollectionCard/Context.tsx +++ b/projects/app/src/pageComponents/dataset/detail/CollectionCard/Context.tsx @@ -129,13 +129,13 @@ const CollectionPageContextProvider = ({ children }: { children: ReactNode }) => pageSize } = usePagination(getDatasetCollections, { defaultPageSize: 20, + storeToQuery: true, params: { datasetId, parentId, searchText, filterTags }, - // defaultRequest: false, refreshDeps: [parentId, searchText, filterTags] }); diff --git a/projects/app/src/pageComponents/dataset/detail/Import/commonProgress/Upload.tsx b/projects/app/src/pageComponents/dataset/detail/Import/commonProgress/Upload.tsx index 19a0d2df09ed..ea96fc073390 100644 --- a/projects/app/src/pageComponents/dataset/detail/Import/commonProgress/Upload.tsx +++ b/projects/app/src/pageComponents/dataset/detail/Import/commonProgress/Upload.tsx @@ -190,7 +190,7 @@ const Upload = () => { router.replace({ query: { datasetId: datasetDetail._id, - currentTab: TabEnum.collectionCard + parentId } }); }, diff --git a/projects/app/src/pageComponents/dataset/detail/Info/index.tsx b/projects/app/src/pageComponents/dataset/detail/Info/index.tsx index 064a63220ba7..15aabe15c9a5 100644 --- a/projects/app/src/pageComponents/dataset/detail/Info/index.tsx +++ b/projects/app/src/pageComponents/dataset/detail/Info/index.tsx @@ -29,6 +29,7 @@ import dynamic from 'next/dynamic'; import type { EditAPIDatasetInfoFormType } from './components/EditApiServiceModal'; import { type EditResourceInfoFormType } from '@/components/common/Modal/EditResourceModal'; import MyTooltip from '@fastgpt/web/components/common/MyTooltip'; +import { ReadRoleVal } from '@fastgpt/global/support/permission/constant'; const EditResourceModal = dynamic(() => import('@/components/common/Modal/EditResourceModal')); const EditAPIDatasetInfoModal = dynamic(() => import('./components/EditApiServiceModal')); @@ -380,6 +381,7 @@ const Info = ({ datasetId }: { datasetId: string }) => { getCollaboratorList(datasetId), roleList: DatasetRoleList, diff --git a/projects/app/src/pageComponents/dataset/detail/NavBar.tsx b/projects/app/src/pageComponents/dataset/detail/NavBar.tsx index 8fc1080537cd..eb37f294c5d5 100644 --- a/projects/app/src/pageComponents/dataset/detail/NavBar.tsx +++ b/projects/app/src/pageComponents/dataset/detail/NavBar.tsx @@ -137,13 +137,7 @@ const NavBar = ({ currentTab }: { currentTab: TabEnum }) => { fontSize={'sm'} fontWeight={500} onClick={() => { - router.replace({ - query: { - datasetId: router.query.datasetId, - parentId: router.query.parentId, - currentTab: TabEnum.collectionCard - } - }); + router.back(); }} > import('@/components/common/Modal/EditResourceModal')); @@ -54,7 +55,7 @@ function List() { const router = useRouter(); const { parentId = null } = router.query as { parentId?: string | null }; const parentDataset = useMemo( - () => myDatasets.find((item) => String(item._id) === parentId), + () => myDatasets.find((item) => item._id === parentId), [parentId, myDatasets] ); @@ -81,7 +82,7 @@ function List() { }); const editPerDataset = useMemo( - () => myDatasets.find((item) => String(item._id) === String(editPerDatasetId)), + () => myDatasets.find((item) => item._id === editPerDatasetId), [editPerDatasetId, myDatasets] ); @@ -433,6 +434,7 @@ function List() { avatar={editPerDataset.avatar} name={editPerDataset.name} managePer={{ + defaultRole: ReadRoleVal, permission: editPerDataset.permission, onGetCollaboratorList: () => getCollaboratorList(editPerDataset._id), roleList: DatasetRoleList, diff --git a/projects/app/src/pages/api/admin/initv4124.ts b/projects/app/src/pages/api/admin/initv4124.ts new file mode 100644 index 000000000000..5f0ddb600a8d --- /dev/null +++ b/projects/app/src/pages/api/admin/initv4124.ts @@ -0,0 +1,86 @@ +import { NextAPI } from '@/service/middleware/entry'; +import { OwnerRoleVal, PerResourceTypeEnum } from '@fastgpt/global/support/permission/constant'; +import { MongoApp } from '@fastgpt/service/core/app/schema'; +import { MongoDataset } from '@fastgpt/service/core/dataset/schema'; +import { authCert } from '@fastgpt/service/support/permission/auth/common'; +import { MongoResourcePermission } from '@fastgpt/service/support/permission/schema'; +import { MongoTeamMember } from '@fastgpt/service/support/user/team/teamMemberSchema'; +import type { ApiRequestProps, ApiResponseType } from '@fastgpt/service/type/next'; + +export type SyncAppChatLogQuery = {}; + +export type SyncAppChatLogBody = { + batchSize?: number; +}; + +export type SyncAppChatLogResponse = {}; + +/** + * 初始化脚本 v4.13.0 + * 对系统内所有资源 App 和 dataset 添加 tmbId 为自己 owner 的协作者,权限为 OwnerRoleVal + */ +async function handler( + req: ApiRequestProps, + res: ApiResponseType +) { + await authCert({ req, authRoot: true }); + + // find all resources + const [apps, datasets, tmbs] = await Promise.all([ + MongoApp.find({}, '_id teamId tmbId').lean(), + MongoDataset.find({}, '_id teamId tmbId').lean(), + MongoTeamMember.find({ role: 'owner' }, '_id teamId').lean() + ]); + + await MongoResourcePermission.bulkWrite( + apps.map((app) => ({ + updateOne: { + filter: { + resourceId: app._id, + resourceType: PerResourceTypeEnum.app, + teamId: app.teamId, + tmbId: app.tmbId + }, + update: { + permission: OwnerRoleVal + }, + upsert: true + } + })) + ); + + await MongoResourcePermission.bulkWrite( + datasets.map((dataset) => ({ + updateOne: { + filter: { + resourceId: dataset._id, + resourceType: PerResourceTypeEnum.dataset, + teamId: dataset.teamId, + tmbId: dataset.tmbId + }, + update: { + permission: OwnerRoleVal + }, + upsert: true + } + })) + ); + + await MongoResourcePermission.bulkWrite( + tmbs.map((team) => ({ + deleteOne: { + filter: { + resourceType: PerResourceTypeEnum.team, + teamId: team.teamId, + tmbId: team._id + } + } + })) + ); + + return { + message: 'Success' + }; +} + +export default NextAPI(handler); diff --git a/projects/app/src/pages/api/common/file/read.ts b/projects/app/src/pages/api/common/file/read.ts index 0f84b5e955a0..c81a4b0def0b 100644 --- a/projects/app/src/pages/api/common/file/read.ts +++ b/projects/app/src/pages/api/common/file/read.ts @@ -1,9 +1,9 @@ import type { NextApiRequest, NextApiResponse } from 'next'; import { jsonRes } from '@fastgpt/service/common/response'; -import { authFileToken } from '@fastgpt/service/support/permission/controller'; import { getDownloadStream, getFileById } from '@fastgpt/service/common/file/gridfs/controller'; import { CommonErrEnum } from '@fastgpt/global/common/error/code/common'; import { stream2Encoding } from '@fastgpt/service/common/file/gridfs/utils'; +import { authFileToken } from '@fastgpt/service/support/permission/auth/file'; const previewableExtensions = [ 'jpg', diff --git a/projects/app/src/pages/api/common/file/read/[filename].ts b/projects/app/src/pages/api/common/file/read/[filename].ts index ba715fbe9398..fbd9d20b5879 100644 --- a/projects/app/src/pages/api/common/file/read/[filename].ts +++ b/projects/app/src/pages/api/common/file/read/[filename].ts @@ -1,9 +1,9 @@ import type { NextApiRequest, NextApiResponse } from 'next'; import { jsonRes } from '@fastgpt/service/common/response'; -import { authFileToken } from '@fastgpt/service/support/permission/controller'; import { getDownloadStream, getFileById } from '@fastgpt/service/common/file/gridfs/controller'; import { CommonErrEnum } from '@fastgpt/global/common/error/code/common'; import { stream2Encoding } from '@fastgpt/service/common/file/gridfs/utils'; +import { authFileToken } from '@fastgpt/service/support/permission/auth/file'; const previewableExtensions = [ 'jpg', diff --git a/projects/app/src/pages/api/common/file/upload.ts b/projects/app/src/pages/api/common/file/upload.ts index 63340f78488b..e0845a2b92b3 100644 --- a/projects/app/src/pages/api/common/file/upload.ts +++ b/projects/app/src/pages/api/common/file/upload.ts @@ -4,7 +4,6 @@ import { uploadFile } from '@fastgpt/service/common/file/gridfs/controller'; import { getUploadModel } from '@fastgpt/service/common/file/multer'; import { removeFilesByPaths } from '@fastgpt/service/common/file/utils'; import { NextAPI } from '@/service/middleware/entry'; -import { createFileToken } from '@fastgpt/service/support/permission/controller'; import { ReadFileBaseUrl } from '@fastgpt/global/common/file/constants'; import { addLog } from '@fastgpt/service/common/system/log'; import { authFrequencyLimit } from '@/service/common/frequencyLimit/api'; @@ -13,6 +12,7 @@ import { authChatCrud } from '@/service/support/permission/auth/chat'; import { authDataset } from '@fastgpt/service/support/permission/dataset/auth'; import { type OutLinkChatAuthProps } from '@fastgpt/global/support/permission/chat'; import { WritePermissionVal } from '@fastgpt/global/support/permission/constant'; +import { createFileToken } from '@fastgpt/service/support/permission/auth/file'; export type UploadChatFileProps = { appId: string; diff --git a/projects/app/src/pages/api/core/app/create.ts b/projects/app/src/pages/api/core/app/create.ts index bcfdb9f58588..cad64986253a 100644 --- a/projects/app/src/pages/api/core/app/create.ts +++ b/projects/app/src/pages/api/core/app/create.ts @@ -6,7 +6,11 @@ import type { AppTypeEnum } from '@fastgpt/global/core/app/constants'; import { AppFolderTypeList } from '@fastgpt/global/core/app/constants'; import type { AppSchema } from '@fastgpt/global/core/app/type'; import { type ShortUrlParams } from '@fastgpt/global/support/marketing/type'; -import { WritePermissionVal } from '@fastgpt/global/support/permission/constant'; +import { + OwnerRoleVal, + PerResourceTypeEnum, + WritePermissionVal +} from '@fastgpt/global/support/permission/constant'; import { TeamAppCreatePermissionVal } from '@fastgpt/global/support/permission/user/constant'; import { refreshSourceAvatar } from '@fastgpt/service/common/file/image/controller'; import { pushTrack } from '@fastgpt/service/common/middle/tracks/utils'; @@ -22,6 +26,7 @@ import { type ApiRequestProps } from '@fastgpt/service/type/next'; import { addAuditLog } from '@fastgpt/service/support/user/audit/util'; import { AuditEventEnum } from '@fastgpt/global/support/user/audit/constants'; import { getI18nAppType } from '@fastgpt/service/support/user/audit/util'; +import { MongoResourcePermission } from '@fastgpt/service/support/permission/schema'; export type CreateAppBody = { parentId?: ParentIdType; @@ -113,7 +118,7 @@ export const onCreateApp = async ({ session?: ClientSession; }) => { const create = async (session: ClientSession) => { - const [{ _id: appId }] = await MongoApp.create( + const [app] = await MongoApp.create( [ { ...parseParentIdInMongo(parentId), @@ -133,6 +138,8 @@ export const onCreateApp = async ({ { session, ordered: true } ); + const appId = app._id; + if (!AppFolderTypeList.includes(type!)) { await MongoAppVersion.create( [ @@ -151,6 +158,15 @@ export const onCreateApp = async ({ { session, ordered: true } ); } + + await MongoResourcePermission.insertOne({ + teamId, + tmbId, + resourceId: app._id, + permission: OwnerRoleVal, + resourceType: PerResourceTypeEnum.app + }); + (async () => { addAuditLog({ tmbId, diff --git a/projects/app/src/pages/api/core/app/folder/create.ts b/projects/app/src/pages/api/core/app/folder/create.ts index 88ff0d7e9b1c..b986a7b19812 100644 --- a/projects/app/src/pages/api/core/app/folder/create.ts +++ b/projects/app/src/pages/api/core/app/folder/create.ts @@ -3,9 +3,8 @@ import { CommonErrEnum } from '@fastgpt/global/common/error/code/common'; import { FolderImgUrl } from '@fastgpt/global/common/file/image/constants'; import { type ParentIdType } from '@fastgpt/global/common/parentFolder/type'; import { parseParentIdInMongo } from '@fastgpt/global/common/parentFolder/utils'; -import { AppTypeEnum } from '@fastgpt/global/core/app/constants'; +import { AppFolderTypeList, AppTypeEnum } from '@fastgpt/global/core/app/constants'; import { - OwnerPermissionVal, PerResourceTypeEnum, WritePermissionVal } from '@fastgpt/global/support/permission/constant'; @@ -13,9 +12,7 @@ import { TeamAppCreatePermissionVal } from '@fastgpt/global/support/permission/u import { mongoSessionRun } from '@fastgpt/service/common/mongo/sessionRun'; import { MongoApp } from '@fastgpt/service/core/app/schema'; import { authApp } from '@fastgpt/service/support/permission/app/auth'; -import { getResourceClbsAndGroups } from '@fastgpt/service/support/permission/controller'; -import { syncCollaborators } from '@fastgpt/service/support/permission/inheritPermission'; -import { MongoResourcePermission } from '@fastgpt/service/support/permission/schema'; +import { createResourceDefaultCollaborators } from '@fastgpt/service/support/permission/controller'; import { authUserPer } from '@fastgpt/service/support/permission/user/auth'; import { type ApiRequestProps } from '@fastgpt/service/type/next'; import { addAuditLog } from '@fastgpt/service/support/user/audit/util'; @@ -50,39 +47,12 @@ async function handler(req: ApiRequestProps) { type: AppTypeEnum.folder }); - if (parentId) { - const parentClbsAndGroups = await getResourceClbsAndGroups({ - teamId, - resourceId: parentId, - resourceType: PerResourceTypeEnum.app, - session - }); - - await syncCollaborators({ - resourceType: PerResourceTypeEnum.app, - teamId, - resourceId: app._id, - collaborators: parentClbsAndGroups, - session - }); - } else { - // Create default permission - await MongoResourcePermission.create( - [ - { - resourceType: PerResourceTypeEnum.app, - teamId, - resourceId: app._id, - tmbId, - permission: OwnerPermissionVal - } - ], - { - session, - ordered: true - } - ); - } + await createResourceDefaultCollaborators({ + tmbId, + session, + resource: app, + resourceType: PerResourceTypeEnum.app + }); }); (async () => { addAuditLog({ diff --git a/projects/app/src/pages/api/core/app/list.ts b/projects/app/src/pages/api/core/app/list.ts index 17839ef128d1..f5c33a57a23c 100644 --- a/projects/app/src/pages/api/core/app/list.ts +++ b/projects/app/src/pages/api/core/app/list.ts @@ -179,7 +179,7 @@ async function handler(req: ApiRequestProps): Promise String(item.resourceId) === appId && !!item.tmbId )?.permission; - const groupRole = sumPer( + const groupAndOrgRole = sumPer( ...myPerList .filter( (item) => String(item.resourceId) === appId && (!!item.groupId || !!item.orgId) @@ -188,7 +188,7 @@ async function handler(req: ApiRequestProps): Promise): Promise String(item.resourceId) === String(appId)).length; }; - // Inherit app, check parent folder clb + // Inherit app, check parent folder clb and it's own clb if (!AppFolderTypeList.includes(app.type) && app.parentId && app.inheritPermission) { return { - Per: getPer(String(app.parentId)), + Per: getPer(String(app.parentId)).addRole(getPer(String(app._id)).role), privateApp: getClbCount(String(app.parentId)) <= 1 }; } return { Per: getPer(String(app._id)), - privateApp: AppFolderTypeList.includes(app.type) - ? getClbCount(String(app._id)) <= 1 - : getClbCount(String(app._id)) === 0 + privateApp: getClbCount(String(app._id)) <= 1 }; })(); diff --git a/projects/app/src/pages/api/core/app/update.ts b/projects/app/src/pages/api/core/app/update.ts index a58f8f86f3ce..5709f8ccc7f9 100644 --- a/projects/app/src/pages/api/core/app/update.ts +++ b/projects/app/src/pages/api/core/app/update.ts @@ -18,12 +18,11 @@ import { import { AppFolderTypeList, AppTypeEnum } from '@fastgpt/global/core/app/constants'; import { type ClientSession } from 'mongoose'; import { mongoSessionRun } from '@fastgpt/service/common/mongo/sessionRun'; -import { getResourceClbsAndGroups } from '@fastgpt/service/support/permission/controller'; +import { getResourceOwnedClbs } from '@fastgpt/service/support/permission/controller'; import { authUserPer } from '@fastgpt/service/support/permission/user/auth'; import { TeamAppCreatePermissionVal } from '@fastgpt/global/support/permission/user/constant'; import { AppErrEnum } from '@fastgpt/global/common/error/code/app'; import { refreshSourceAvatar } from '@fastgpt/service/common/file/image/controller'; -import { MongoResourcePermission } from '@fastgpt/service/support/permission/schema'; import { addAuditLog } from '@fastgpt/service/support/user/audit/util'; import { AuditEventEnum } from '@fastgpt/global/support/user/audit/constants'; import { getI18nAppType } from '@fastgpt/service/support/user/audit/util'; @@ -150,38 +149,30 @@ async function handler(req: ApiRequestProps) { if (isMove) { await mongoSessionRun(async (session) => { // Inherit folder: Sync children permission and it's clbs - if (AppFolderTypeList.includes(app.type)) { - const parentClbsAndGroups = await getResourceClbsAndGroups({ - teamId: app.teamId, - resourceId: parentId, - resourceType: PerResourceTypeEnum.app, - session - }); - // sync self - await syncCollaborators({ - resourceId: app._id, - resourceType: PerResourceTypeEnum.app, - collaborators: parentClbsAndGroups, - session, - teamId: app.teamId - }); - // sync the children - await syncChildrenPermission({ - resource: app, - resourceType: PerResourceTypeEnum.app, - resourceModel: MongoApp, - folderTypeList: AppFolderTypeList, - collaborators: parentClbsAndGroups, - session - }); - } else { - logAppMove({ tmbId, teamId, app, targetName }); - // Not folder, delete all clb - await MongoResourcePermission.deleteMany( - { resourceType: PerResourceTypeEnum.app, teamId: app.teamId, resourceId: app._id }, - { session } - ); - } + const parentClbs = await getResourceOwnedClbs({ + teamId: app.teamId, + resourceId: parentId, + resourceType: PerResourceTypeEnum.app, + session + }); + // sync self + await syncCollaborators({ + resourceId: app._id, + resourceType: PerResourceTypeEnum.app, + collaborators: parentClbs, + session, + teamId: app.teamId + }); + // sync the children + await syncChildrenPermission({ + resource: app, + resourceType: PerResourceTypeEnum.app, + resourceModel: MongoApp, + folderTypeList: AppFolderTypeList, + collaborators: parentClbs, + session + }); + logAppMove({ tmbId, teamId, app, targetName }); return onUpdate(session); }); } else { diff --git a/projects/app/src/pages/api/core/dataset/collection/delete.ts b/projects/app/src/pages/api/core/dataset/collection/delete.ts index 003ca6f44a65..36488944f40a 100644 --- a/projects/app/src/pages/api/core/dataset/collection/delete.ts +++ b/projects/app/src/pages/api/core/dataset/collection/delete.ts @@ -14,15 +14,18 @@ export type DelCollectionBody = { collectionIds: string[]; }; -async function handler(req: ApiRequestProps) { +async function handler(req: ApiRequestProps) { + const id = req.query.id; const { collectionIds } = req.body; - if (!collectionIds) { + const deletedIds = id ? [id] : collectionIds; + + if (!Array.isArray(deletedIds)) { return Promise.reject(CommonErrEnum.missingParams); } const [{ teamId, collection, tmbId }] = await Promise.all( - collectionIds.map(async (collectionId) => { + deletedIds.map(async (collectionId) => { return await authDatasetCollection({ req, authToken: true, @@ -35,7 +38,7 @@ async function handler(req: ApiRequestProps) { // find all delete id const collections = await Promise.all( - collectionIds.map(async (collectionId) => { + deletedIds.map(async (collectionId) => { return await findCollectionAndChild({ teamId, datasetId: collection.datasetId, @@ -45,7 +48,6 @@ async function handler(req: ApiRequestProps) { }) ).then((res) => { const flattened = res.flat(); - console.log(flattened.length, 22); // Remove duplicates based on _id const uniqueCollections = flattened.filter( (collection, index, arr) => diff --git a/projects/app/src/pages/api/core/dataset/collection/listV2.ts b/projects/app/src/pages/api/core/dataset/collection/listV2.ts index f510b18ea8fb..0dfbc7114d59 100644 --- a/projects/app/src/pages/api/core/dataset/collection/listV2.ts +++ b/projects/app/src/pages/api/core/dataset/collection/listV2.ts @@ -111,7 +111,7 @@ async function handler( .lean(), MongoDatasetCollection.countDocuments(match, { ...readFromSecondary }) ]); - const collectionIds = collections.map((item) => item._id); + const collectionIds = collections.map((item) => new Types.ObjectId(item._id)); // Compute data amount const [trainingAmount, dataAmount]: [ @@ -122,8 +122,8 @@ async function handler( [ { $match: { - teamId: match.teamId, - datasetId: match.datasetId, + teamId: new Types.ObjectId(teamId), + datasetId: new Types.ObjectId(datasetId), collectionId: { $in: collectionIds } } }, @@ -143,8 +143,8 @@ async function handler( [ { $match: { - teamId: match.teamId, - datasetId: match.datasetId, + teamId: new Types.ObjectId(teamId), + datasetId: new Types.ObjectId(datasetId), collectionId: { $in: collectionIds } } }, diff --git a/projects/app/src/pages/api/core/dataset/collection/read.ts b/projects/app/src/pages/api/core/dataset/collection/read.ts index 19935b3c2c2f..ed510848a04f 100644 --- a/projects/app/src/pages/api/core/dataset/collection/read.ts +++ b/projects/app/src/pages/api/core/dataset/collection/read.ts @@ -2,7 +2,6 @@ import type { ApiRequestProps } from '@fastgpt/service/type/next'; import { NextAPI } from '@/service/middleware/entry'; import { authDatasetCollection } from '@fastgpt/service/support/permission/dataset/auth'; import { DatasetCollectionTypeEnum } from '@fastgpt/global/core/dataset/constants'; -import { createFileToken } from '@fastgpt/service/support/permission/controller'; import { BucketNameEnum, ReadFileBaseUrl } from '@fastgpt/global/common/file/constants'; import { ReadPermissionVal } from '@fastgpt/global/support/permission/constant'; import { type OutLinkChatAuthProps } from '@fastgpt/global/support/permission/chat'; @@ -10,6 +9,7 @@ import { DatasetErrEnum } from '@fastgpt/global/common/error/code/dataset'; import { authChatCrud, authCollectionInChat } from '@/service/support/permission/auth/chat'; import { getCollectionWithDataset } from '@fastgpt/service/core/dataset/controller'; import { getApiDatasetRequest } from '@fastgpt/service/core/dataset/apiDataset'; +import { createFileToken } from '@fastgpt/service/support/permission/auth/file'; export type readCollectionSourceQuery = {}; @@ -48,7 +48,7 @@ async function handler( }); } - /* + /* 1. auth chat read permission 2. auth collection quote in chat 3. auth outlink open show quote diff --git a/projects/app/src/pages/api/core/dataset/collection/trainingDetail.ts b/projects/app/src/pages/api/core/dataset/collection/trainingDetail.ts index 76b9abfd27d0..0e54817a2a45 100644 --- a/projects/app/src/pages/api/core/dataset/collection/trainingDetail.ts +++ b/projects/app/src/pages/api/core/dataset/collection/trainingDetail.ts @@ -9,6 +9,7 @@ import { ReadPermissionVal } from '@fastgpt/global/support/permission/constant'; import { authDatasetCollection } from '@fastgpt/service/support/permission/dataset/auth'; import { MongoDatasetData } from '@fastgpt/service/core/dataset/data/schema'; import { type ApiRequestProps } from '@fastgpt/service/type/next'; +import { Types } from '@fastgpt/service/common/mongo'; type getTrainingDetailParams = { collectionId: string; @@ -50,9 +51,9 @@ async function handler( }); const match = { - teamId: collection.teamId, - datasetId: collection.datasetId, - collectionId: collection._id + teamId: new Types.ObjectId(collection.teamId), + datasetId: new Types.ObjectId(collection.datasetId), + collectionId: new Types.ObjectId(collection._id) }; // Computed global queue @@ -74,7 +75,7 @@ async function handler( [ { $match: { - _id: { $lt: minId }, + _id: { $lt: new Types.ObjectId(minId) }, retryCount: { $gt: 0 }, lockTime: { $lt: new Date('2050/1/1') } } diff --git a/projects/app/src/pages/api/core/dataset/create.ts b/projects/app/src/pages/api/core/dataset/create.ts index 71ec9b53a9f0..482d3f623d35 100644 --- a/projects/app/src/pages/api/core/dataset/create.ts +++ b/projects/app/src/pages/api/core/dataset/create.ts @@ -2,7 +2,11 @@ import type { CreateDatasetParams } from '@/global/core/dataset/api.d'; import { NextAPI } from '@/service/middleware/entry'; import { parseParentIdInMongo } from '@fastgpt/global/common/parentFolder/utils'; import { DatasetTypeEnum } from '@fastgpt/global/core/dataset/constants'; -import { WritePermissionVal } from '@fastgpt/global/support/permission/constant'; +import { + OwnerRoleVal, + PerResourceTypeEnum, + WritePermissionVal +} from '@fastgpt/global/support/permission/constant'; import { TeamDatasetCreatePermissionVal } from '@fastgpt/global/support/permission/user/constant'; import { refreshSourceAvatar } from '@fastgpt/service/common/file/image/controller'; import { pushTrack } from '@fastgpt/service/common/middle/tracks/utils'; @@ -21,6 +25,7 @@ import type { ApiRequestProps } from '@fastgpt/service/type/next'; import { addAuditLog } from '@fastgpt/service/support/user/audit/util'; import { AuditEventEnum } from '@fastgpt/global/support/user/audit/constants'; import { getI18nDatasetType } from '@fastgpt/service/support/user/audit/util'; +import { MongoResourcePermission } from '@fastgpt/service/support/permission/schema'; export type DatasetCreateQuery = {}; export type DatasetCreateBody = CreateDatasetParams; @@ -71,7 +76,7 @@ async function handler( await checkTeamDatasetLimit(teamId); const datasetId = await mongoSessionRun(async (session) => { - const [{ _id }] = await MongoDataset.create( + const [dataset] = await MongoDataset.create( [ { ...parseParentIdInMongo(parentId), @@ -89,9 +94,18 @@ async function handler( ], { session, ordered: true } ); + + await MongoResourcePermission.insertOne({ + teamId, + tmbId, + resourceId: dataset._id, + permission: OwnerRoleVal, + resourceType: PerResourceTypeEnum.dataset + }); + await refreshSourceAvatar(avatar, undefined, session); - return _id; + return dataset._id; }); pushTrack.createDataset({ diff --git a/projects/app/src/pages/api/core/dataset/folder/create.ts b/projects/app/src/pages/api/core/dataset/folder/create.ts index 3bb34ad2252e..426db0a7f864 100644 --- a/projects/app/src/pages/api/core/dataset/folder/create.ts +++ b/projects/app/src/pages/api/core/dataset/folder/create.ts @@ -4,17 +4,14 @@ import { FolderImgUrl } from '@fastgpt/global/common/file/image/constants'; import { parseParentIdInMongo } from '@fastgpt/global/common/parentFolder/utils'; import { DatasetTypeEnum } from '@fastgpt/global/core/dataset/constants'; import { - OwnerPermissionVal, PerResourceTypeEnum, WritePermissionVal } from '@fastgpt/global/support/permission/constant'; import { TeamDatasetCreatePermissionVal } from '@fastgpt/global/support/permission/user/constant'; import { mongoSessionRun } from '@fastgpt/service/common/mongo/sessionRun'; import { MongoDataset } from '@fastgpt/service/core/dataset/schema'; -import { getResourceClbsAndGroups } from '@fastgpt/service/support/permission/controller'; +import { createResourceDefaultCollaborators } from '@fastgpt/service/support/permission/controller'; import { authDataset } from '@fastgpt/service/support/permission/dataset/auth'; -import { syncCollaborators } from '@fastgpt/service/support/permission/inheritPermission'; -import { MongoResourcePermission } from '@fastgpt/service/support/permission/schema'; import { authUserPer } from '@fastgpt/service/support/permission/user/auth'; import type { ApiRequestProps, ApiResponseType } from '@fastgpt/service/type/next'; import { addAuditLog } from '@fastgpt/service/support/user/audit/util'; @@ -62,37 +59,12 @@ async function handler( type: DatasetTypeEnum.folder }); - if (parentId) { - const parentClbsAndGroups = await getResourceClbsAndGroups({ - teamId, - resourceId: parentId, - resourceType: PerResourceTypeEnum.dataset, - session - }); - - await syncCollaborators({ - resourceType: PerResourceTypeEnum.dataset, - teamId, - resourceId: dataset._id, - collaborators: parentClbsAndGroups, - session - }); - } - - if (!parentId) { - await MongoResourcePermission.create( - [ - { - resourceType: PerResourceTypeEnum.dataset, - teamId, - resourceId: dataset._id, - tmbId, - permission: OwnerPermissionVal - } - ], - { session, ordered: true } - ); - } + await createResourceDefaultCollaborators({ + tmbId, + session, + resource: dataset, + resourceType: PerResourceTypeEnum.dataset + }); }); (async () => { addAuditLog({ diff --git a/projects/app/src/pages/api/core/dataset/list.ts b/projects/app/src/pages/api/core/dataset/list.ts index 7fce507c0690..e1cec53619b3 100644 --- a/projects/app/src/pages/api/core/dataset/list.ts +++ b/projects/app/src/pages/api/core/dataset/list.ts @@ -132,7 +132,7 @@ async function handler(req: ApiRequestProps) { const tmbRole = myRoles.find( (item) => String(item.resourceId) === datasetId && !!item.tmbId )?.permission; - const groupRole = sumPer( + const groupAndOrgRole = sumPer( ...myRoles .filter( (item) => String(item.resourceId) === datasetId && (!!item.groupId || !!item.orgId) @@ -140,7 +140,7 @@ async function handler(req: ApiRequestProps) { .map((item) => item.permission) ); return new DatasetPermission({ - role: tmbRole ?? groupRole, + role: tmbRole ?? groupAndOrgRole, isOwner: String(dataset.tmbId) === String(tmbId) || teamPer.isOwner }); }; @@ -155,16 +155,13 @@ async function handler(req: ApiRequestProps) { dataset.type !== DatasetTypeEnum.folder ) { return { - Per: getPer(String(dataset.parentId)), + Per: getPer(String(dataset.parentId)).addRole(getPer(String(dataset._id)).role), privateDataset: getClbCount(String(dataset.parentId)) <= 1 }; } return { Per: getPer(String(dataset._id)), - privateDataset: - dataset.type === DatasetTypeEnum.folder - ? getClbCount(String(dataset._id)) <= 1 - : getClbCount(String(dataset._id)) === 0 + privateDataset: getClbCount(String(dataset._id)) <= 1 }; })(); diff --git a/projects/app/src/pages/api/core/dataset/update.ts b/projects/app/src/pages/api/core/dataset/update.ts index 0d679c4a09c3..a2b0611ad353 100644 --- a/projects/app/src/pages/api/core/dataset/update.ts +++ b/projects/app/src/pages/api/core/dataset/update.ts @@ -17,7 +17,6 @@ import { import { type ClientSession } from 'mongoose'; import { parseParentIdInMongo } from '@fastgpt/global/common/parentFolder/utils'; import { mongoSessionRun } from '@fastgpt/service/common/mongo/sessionRun'; -import { getResourceClbsAndGroups } from '@fastgpt/service/support/permission/controller'; import { syncChildrenPermission, syncCollaborators @@ -26,10 +25,7 @@ import { authUserPer } from '@fastgpt/service/support/permission/user/auth'; import { TeamDatasetCreatePermissionVal } from '@fastgpt/global/support/permission/user/constant'; import { DatasetErrEnum } from '@fastgpt/global/common/error/code/dataset'; import { MongoDatasetTraining } from '@fastgpt/service/core/dataset/training/schema'; -import { MongoDatasetCollection } from '@fastgpt/service/core/dataset/collection/schema'; -import { addDays } from 'date-fns'; import { refreshSourceAvatar } from '@fastgpt/service/common/file/image/controller'; -import { MongoResourcePermission } from '@fastgpt/service/support/permission/schema'; import { type DatasetSchemaType } from '@fastgpt/global/core/dataset/type'; import { removeDatasetSyncJobScheduler, @@ -42,6 +38,7 @@ import { AuditEventEnum } from '@fastgpt/global/support/user/audit/constants'; import { getI18nDatasetType } from '@fastgpt/service/support/user/audit/util'; import { getEmbeddingModel, getLLMModel } from '@fastgpt/service/core/ai/model'; import { computedCollectionChunkSettings } from '@fastgpt/global/core/dataset/training/utils'; +import { getResourceOwnedClbs } from '@fastgpt/service/support/permission/controller'; export type DatasetUpdateQuery = {}; export type DatasetUpdateResponse = any; @@ -233,39 +230,30 @@ async function handler( await mongoSessionRun(async (session) => { if (isMove) { - if (isFolder && dataset.inheritPermission) { - const parentClbsAndGroups = await getResourceClbsAndGroups({ - teamId: dataset.teamId, - resourceId: parentId, - resourceType: PerResourceTypeEnum.dataset, - session - }); + const parentClbs = await getResourceOwnedClbs({ + teamId: dataset.teamId, + resourceId: parentId, + resourceType: PerResourceTypeEnum.dataset, + session + }); - await syncCollaborators({ - teamId: dataset.teamId, - resourceId: id, - resourceType: PerResourceTypeEnum.dataset, - collaborators: parentClbsAndGroups, - session - }); + await syncCollaborators({ + teamId: dataset.teamId, + resourceId: id, + resourceType: PerResourceTypeEnum.dataset, + collaborators: parentClbs, + session + }); - await syncChildrenPermission({ - resource: dataset, - resourceType: PerResourceTypeEnum.dataset, - resourceModel: MongoDataset, - folderTypeList: [DatasetTypeEnum.folder], - collaborators: parentClbsAndGroups, - session - }); - logDatasetMove({ tmbId, teamId, dataset, targetName }); - } else { - logDatasetMove({ tmbId, teamId, dataset, targetName }); - // Not folder, delete all clb - await MongoResourcePermission.deleteMany( - { resourceId: id, teamId: dataset.teamId, resourceType: PerResourceTypeEnum.dataset }, - { session } - ); - } + await syncChildrenPermission({ + resource: dataset, + resourceType: PerResourceTypeEnum.dataset, + resourceModel: MongoDataset, + folderTypeList: [DatasetTypeEnum.folder], + collaborators: parentClbs, + session + }); + logDatasetMove({ tmbId, teamId, dataset, targetName }); return onUpdate(session); } else { logDatasetUpdate({ tmbId, teamId, dataset }); diff --git a/projects/app/src/pages/api/core/workflow/debug.ts b/projects/app/src/pages/api/core/workflow/debug.ts index c54e6a620b41..a2d35c8cb25d 100644 --- a/projects/app/src/pages/api/core/workflow/debug.ts +++ b/projects/app/src/pages/api/core/workflow/debug.ts @@ -9,7 +9,6 @@ import { getRunningUserInfoByTmbId } from '@fastgpt/service/support/user/team/ut import type { PostWorkflowDebugProps, PostWorkflowDebugResponse } from '@/global/core/workflow/api'; import { NextAPI } from '@/service/middleware/entry'; import { ReadPermissionVal } from '@fastgpt/global/support/permission/constant'; -import { defaultApp } from '@/web/core/app/constants'; import { WORKFLOW_MAX_RUN_TIMES } from '@fastgpt/service/core/workflow/constants'; import { getLastInteractiveValue } from '@fastgpt/global/core/workflow/runtime/utils'; import { getLocale } from '@fastgpt/service/common/middle/i18n'; @@ -25,7 +24,8 @@ async function handler( variables = {}, appId, query = [], - history = [] + history = [], + chatConfig } = req.body as PostWorkflowDebugProps; if (!nodes) { return Promise.reject('Prams Error'); @@ -71,7 +71,7 @@ async function handler( lastInteractive: interactive, variables, query: query, - chatConfig: defaultApp.chatConfig, + chatConfig: chatConfig || app.chatConfig, histories: history, stream: false, maxRunTimes: WORKFLOW_MAX_RUN_TIMES diff --git a/projects/app/src/pages/api/support/outLink/wecom/[token].ts b/projects/app/src/pages/api/support/outLink/wecom/[token].ts index 0b46aba8e491..fd2b6e89a15d 100644 --- a/projects/app/src/pages/api/support/outLink/wecom/[token].ts +++ b/projects/app/src/pages/api/support/outLink/wecom/[token].ts @@ -9,8 +9,6 @@ async function handler( req: ApiRequestProps, res: ApiResponseType ): Promise { - // WARN: it is not supported yet. - return {}; const { token, type } = req.query; const result = await plusRequest({ url: `support/outLink/wecom/${token}`, diff --git a/projects/app/src/pages/api/support/user/account/loginByPassword.ts b/projects/app/src/pages/api/support/user/account/loginByPassword.ts index deaa82583435..d2b11d9faf53 100644 --- a/projects/app/src/pages/api/support/user/account/loginByPassword.ts +++ b/projects/app/src/pages/api/support/user/account/loginByPassword.ts @@ -1,6 +1,5 @@ import type { NextApiRequest, NextApiResponse } from 'next'; import { MongoUser } from '@fastgpt/service/support/user/schema'; -import { setCookie } from '@fastgpt/service/support/permission/controller'; import { getUserDetail } from '@fastgpt/service/support/user/controller'; import type { PostLoginProps } from '@fastgpt/global/support/user/api.d'; import { UserStatusEnum } from '@fastgpt/global/support/user/constant'; @@ -15,6 +14,7 @@ import { UserAuthTypeEnum } from '@fastgpt/global/support/user/auth/constants'; import { authCode } from '@fastgpt/service/support/user/auth/controller'; import { createUserSession } from '@fastgpt/service/support/user/session'; import requestIp from 'request-ip'; +import { setCookie } from '@fastgpt/service/support/permission/auth/common'; async function handler(req: NextApiRequest, res: NextApiResponse) { const { username, password, code } = req.body as PostLoginProps; diff --git a/projects/app/src/pages/api/support/user/account/loginout.ts b/projects/app/src/pages/api/support/user/account/loginout.ts index 423c5a49bde9..5e86d3c66ba1 100644 --- a/projects/app/src/pages/api/support/user/account/loginout.ts +++ b/projects/app/src/pages/api/support/user/account/loginout.ts @@ -1,7 +1,6 @@ import type { NextApiRequest, NextApiResponse } from 'next'; -import { clearCookie } from '@fastgpt/service/support/permission/controller'; import { NextAPI } from '@/service/middleware/entry'; -import { authCert } from '@fastgpt/service/support/permission/auth/common'; +import { authCert, clearCookie } from '@fastgpt/service/support/permission/auth/common'; import { delUserAllSession } from '@fastgpt/service/support/user/session'; async function handler(req: NextApiRequest, res: NextApiResponse) { diff --git a/projects/app/src/pages/api/support/user/account/updatePasswordByOld.ts b/projects/app/src/pages/api/support/user/account/updatePasswordByOld.ts index 19052a30286c..f0b1da3f7d91 100644 --- a/projects/app/src/pages/api/support/user/account/updatePasswordByOld.ts +++ b/projects/app/src/pages/api/support/user/account/updatePasswordByOld.ts @@ -8,7 +8,6 @@ import { NextAPI } from '@/service/middleware/entry'; import { addAuditLog } from '@fastgpt/service/support/user/audit/util'; import { AuditEventEnum } from '@fastgpt/global/support/user/audit/constants'; import { delUserAllSession } from '@fastgpt/service/support/user/session'; -import { parseHeaderCert } from '@fastgpt/service/support/permission/controller'; async function handler(req: NextApiRequest, res: NextApiResponse) { const { oldPsw, newPsw } = req.body as { oldPsw: string; newPsw: string }; diff --git a/projects/app/src/pages/dashboard/apps/index.tsx b/projects/app/src/pages/dashboard/apps/index.tsx index 3079ed7df9fc..c6ebbcd620e4 100644 --- a/projects/app/src/pages/dashboard/apps/index.tsx +++ b/projects/app/src/pages/dashboard/apps/index.tsx @@ -34,6 +34,7 @@ import MCPToolsEditModal from '@/pageComponents/dashboard/apps/MCPToolsEditModal import { getUtmWorkflow } from '@/web/support/marketing/utils'; import { useMount } from 'ahooks'; import SearchInput from '@fastgpt/web/components/common/Input/SearchInput'; +import { ReadRoleVal } from '@fastgpt/global/support/permission/constant'; const CreateModal = dynamic(() => import('@/pageComponents/dashboard/apps/CreateModal')); const EditFolderModal = dynamic( @@ -282,6 +283,7 @@ const MyApps = ({ MenuIcon }: { MenuIcon: JSX.Element }) => { deleteTip={t('app:confirm_delete_folder_tip')} onDelete={() => onDeleFolder(folderDetail._id)} managePer={{ + defaultRole: ReadRoleVal, permission: folderDetail.permission, onGetCollaboratorList: () => getCollaboratorList(folderDetail._id), roleList: AppRoleList, diff --git a/projects/app/src/pages/dataset/list/index.tsx b/projects/app/src/pages/dataset/list/index.tsx index 4b8b5759e776..dedb5f17f8fb 100644 --- a/projects/app/src/pages/dataset/list/index.tsx +++ b/projects/app/src/pages/dataset/list/index.tsx @@ -30,6 +30,7 @@ import { DatasetTypeEnum } from '@fastgpt/global/core/dataset/constants'; import { useToast } from '@fastgpt/web/hooks/useToast'; import MyBox from '@fastgpt/web/components/common/MyBox'; import { useSystemStore } from '@/web/common/system/useSystemStore'; +import { ReadRoleVal } from '@fastgpt/global/support/permission/constant'; const EditFolderModal = dynamic( () => import('@fastgpt/web/components/common/MyModal/EditFolderModal') @@ -254,6 +255,7 @@ const Dataset = () => { }) } managePer={{ + defaultRole: ReadRoleVal, permission: folderDetail.permission, onGetCollaboratorList: () => getCollaboratorList(folderDetail._id), roleList: DatasetRoleList, diff --git a/projects/app/src/service/core/app/utils.ts b/projects/app/src/service/core/app/utils.ts index 2d7ba601d8df..d89c8bf95c1c 100644 --- a/projects/app/src/service/core/app/utils.ts +++ b/projects/app/src/service/core/app/utils.ts @@ -1,6 +1,4 @@ -import { getUserChatInfoAndAuthTeamPoints } from '@fastgpt/service/support/permission/auth/team'; -import { getRunningUserInfoByTmbId } from '@fastgpt/service/support/user/team/utils'; -import { createChatUsage } from '@fastgpt/service/support/wallet/usage/controller'; +import { getErrText } from '@fastgpt/global/common/error/utils'; import { getNextTimeByCronStringAndTimezone } from '@fastgpt/global/common/string/time'; import { getNanoid } from '@fastgpt/global/common/string/tools'; import { delay, retryFn } from '@fastgpt/global/common/system/utils'; @@ -9,6 +7,8 @@ import { ChatRoleEnum, ChatSourceEnum } from '@fastgpt/global/core/chat/constants'; +import { type UserChatItemValueItemType } from '@fastgpt/global/core/chat/type'; +import { DispatchNodeResponseKeyEnum } from '@fastgpt/global/core/workflow/runtime/constants'; import { getWorkflowEntryNodeIds, storeEdges2RuntimeEdges, @@ -17,13 +17,13 @@ import { import { UsageSourceEnum } from '@fastgpt/global/support/wallet/usage/constants'; import { addLog } from '@fastgpt/service/common/system/log'; import { MongoApp } from '@fastgpt/service/core/app/schema'; +import { getAppLatestVersion } from '@fastgpt/service/core/app/version/controller'; +import { saveChat } from '@fastgpt/service/core/chat/saveChat'; import { WORKFLOW_MAX_RUN_TIMES } from '@fastgpt/service/core/workflow/constants'; import { dispatchWorkFlow } from '@fastgpt/service/core/workflow/dispatch'; -import { DispatchNodeResponseKeyEnum } from '@fastgpt/global/core/workflow/runtime/constants'; -import { type UserChatItemValueItemType } from '@fastgpt/global/core/chat/type'; -import { saveChat } from '@fastgpt/service/core/chat/saveChat'; -import { getAppLatestVersion } from '@fastgpt/service/core/app/version/controller'; -import { getErrText } from '@fastgpt/global/common/error/utils'; +import { getUserChatInfoAndAuthTeamPoints } from '@fastgpt/service/support/permission/auth/team'; +import { getRunningUserInfoByTmbId } from '@fastgpt/service/support/user/team/utils'; +import { createChatUsage } from '@fastgpt/service/support/wallet/usage/controller'; export const getScheduleTriggerApp = async () => { addLog.info('Schedule trigger app'); diff --git a/projects/app/src/web/core/app/api/collaborator.ts b/projects/app/src/web/core/app/api/collaborator.ts index fda0564c33a2..27b6ff33cfe3 100644 --- a/projects/app/src/web/core/app/api/collaborator.ts +++ b/projects/app/src/web/core/app/api/collaborator.ts @@ -3,10 +3,10 @@ import type { AppCollaboratorDeleteParams } from '@fastgpt/global/core/app/collaborator'; import { DELETE, GET, POST } from '@/web/common/api/request'; -import type { CollaboratorItemType } from '@fastgpt/global/support/permission/collaborator'; +import type { CollaboratorListType } from '@fastgpt/global/support/permission/collaborator'; export const getCollaboratorList = (appId: string) => - GET('/proApi/core/app/collaborator/list', { appId }); + GET('/proApi/core/app/collaborator/list', { appId }); export const postUpdateAppCollaborators = (body: UpdateAppCollaboratorBody) => POST('/proApi/core/app/collaborator/update', body); diff --git a/projects/app/src/web/core/app/diff.ts b/projects/app/src/web/core/app/diff.ts index bee2fb7efdb8..d1ec54eec288 100644 --- a/projects/app/src/web/core/app/diff.ts +++ b/projects/app/src/web/core/app/diff.ts @@ -8,13 +8,6 @@ const createWorkflowDiffPatcher = () => const diffPatcher = createWorkflowDiffPatcher(); -export const getAppDiffConfig = >( - initialState?: T, - newState?: T -) => { - return diffPatcher.diff(initialState, newState); -}; - export const getAppConfigByDiff = >( initialState?: T, diff?: ReturnType diff --git a/projects/app/src/web/core/dataset/api/collaborator.ts b/projects/app/src/web/core/dataset/api/collaborator.ts index 6c69d73b50c7..6a8336805cf8 100644 --- a/projects/app/src/web/core/dataset/api/collaborator.ts +++ b/projects/app/src/web/core/dataset/api/collaborator.ts @@ -3,10 +3,10 @@ import type { DatasetCollaboratorDeleteParams } from '@fastgpt/global/core/dataset/collaborator'; import { DELETE, GET, POST } from '@/web/common/api/request'; -import type { CollaboratorItemType } from '@fastgpt/global/support/permission/collaborator'; +import type { CollaboratorListType } from '@fastgpt/global/support/permission/collaborator'; export const getCollaboratorList = (datasetId: string) => - GET('/proApi/core/dataset/collaborator/list', { datasetId }); + GET('/proApi/core/dataset/collaborator/list', { datasetId }); export const postUpdateDatasetCollaborators = (body: UpdateDatasetCollaboratorBody) => POST('/proApi/core/dataset/collaborator/update', body); diff --git a/projects/app/src/web/support/user/team/api.ts b/projects/app/src/web/support/user/team/api.ts index 5231f1204a5b..5a07ebda02ba 100644 --- a/projects/app/src/web/support/user/team/api.ts +++ b/projects/app/src/web/support/user/team/api.ts @@ -1,6 +1,7 @@ import { GET, POST, PUT, DELETE } from '@/web/common/api/request'; import type { CollaboratorItemType, + CollaboratorListType, DeletePermissionQuery, UpdateClbPermissionProps } from '@fastgpt/global/support/permission/collaborator'; @@ -26,6 +27,7 @@ import type { InvitationLinkCreateType, InvitationType } from '@fastgpt/service/support/user/team/invitationLink/type'; +import type { PermissionValueType } from '@fastgpt/global/support/permission/type'; /* --------------- team ---------------- */ export const getTeamList = (status: `${TeamMemberSchema['status']}`) => @@ -83,9 +85,15 @@ export const putForbidInvitationLink = (linkId: string) => /* -------------- team collaborator -------------------- */ export const getTeamClbs = () => - GET(`/proApi/support/user/team/collaborator/list`); + GET(`/proApi/support/user/team/collaborator/list`); export const updateMemberPermission = (data: UpdateClbPermissionProps) => - PUT('/proApi/support/user/team/collaborator/update', data); + POST('/proApi/support/user/team/collaborator/update', data); +export const updateOneMemberPermission = (data: { + tmbId?: string; + orgId?: string; + groupId?: string; + permission: PermissionValueType; +}) => PUT('/proApi/support/user/team/collaborator/updateOne', data); export const deleteMemberPermission = (id: DeletePermissionQuery) => DELETE('/proApi/support/user/team/collaborator/delete', id); diff --git a/projects/app/src/web/support/user/team/group/api.ts b/projects/app/src/web/support/user/team/group/api.ts index 9a2975e8328d..aba4f93feee8 100644 --- a/projects/app/src/web/support/user/team/group/api.ts +++ b/projects/app/src/web/support/user/team/group/api.ts @@ -10,11 +10,7 @@ import type { } from '@fastgpt/global/support/user/team/group/api'; export const getGroupList = (data: GetGroupListBody) => - POST[]>('/proApi/support/user/team/group/list', data).then((res) => { - console.log(res); - return res; - }); - + POST[]>('/proApi/support/user/team/group/list', data); export const postCreateGroup = (data: postCreateGroupData) => POST('/proApi/support/user/team/group/create', data); diff --git a/projects/app/test/api/core/app/create.test.ts b/projects/app/test/api/core/app/create.test.ts index 0e470a1d9772..69ae40d7158b 100644 --- a/projects/app/test/api/core/app/create.test.ts +++ b/projects/app/test/api/core/app/create.test.ts @@ -11,13 +11,18 @@ import { describe, expect, it } from 'vitest'; describe('create api', () => { it('should return 200 when create app success', async () => { const users = await getFakeUsers(2); - await MongoResourcePermission.create({ - resourceType: 'team', - teamId: users.members[0].teamId, - resourceId: null, - tmbId: users.members[0].tmbId, - permission: TeamAppCreatePermissionVal - }); + await MongoResourcePermission.findOneAndUpdate( + { + resourceType: 'team', + teamId: users.members[0].teamId, + resourceId: null, + tmbId: users.members[0].tmbId + }, + { + permission: TeamAppCreatePermissionVal + }, + { upsert: true } + ); const res = await Call(createapi.default, { auth: users.members[0], @@ -56,13 +61,18 @@ describe('create api', () => { expect(res3.error).toBe(AppErrEnum.unAuthApp); expect(res3.code).toBe(500); - await MongoResourcePermission.create({ - resourceType: 'app', - teamId: users.members[1].teamId, - resourceId: String(folderId), - tmbId: users.members[1].tmbId, - permission: WritePermissionVal - }); + await MongoResourcePermission.findOneAndUpdate( + { + resourceType: 'app', + teamId: users.members[1].teamId, + resourceId: String(folderId), + tmbId: users.members[1].tmbId + }, + { + permission: WritePermissionVal + }, + { upsert: true } + ); const res4 = await Call(createapi.default, { auth: users.members[1], diff --git a/projects/app/test/api/core/dataset/training/getTrainingDataDetail.test.ts b/projects/app/test/api/core/dataset/training/getTrainingDataDetail.test.ts index d4a0b2793ec5..2c3f672bc29e 100644 --- a/projects/app/test/api/core/dataset/training/getTrainingDataDetail.test.ts +++ b/projects/app/test/api/core/dataset/training/getTrainingDataDetail.test.ts @@ -51,8 +51,8 @@ describe('get training data detail test', () => { expect(res.code).toBe(200); expect(res.data).toBeDefined(); - expect(res.data?._id).toStrictEqual(trainingData._id); - expect(res.data?.datasetId).toStrictEqual(dataset._id); + expect(res.data?._id).toStrictEqual(String(trainingData._id)); + expect(res.data?.datasetId).toStrictEqual(String(dataset._id)); expect(res.data?.mode).toBe(TrainingModeEnum.chunk); expect(res.data?.q).toBe('test'); expect(res.data?.a).toBe('test'); diff --git a/test/cases/components/Markdown/utils.test.ts b/projects/app/test/components/Markdown/utils.test.ts similarity index 100% rename from test/cases/components/Markdown/utils.test.ts rename to projects/app/test/components/Markdown/utils.test.ts diff --git a/test/cases/pageComponents/app/detail/WorkflowComponents/utils.test.ts b/projects/app/test/pageComponents/app/detail/WorkflowComponents/utils.test.ts similarity index 100% rename from test/cases/pageComponents/app/detail/WorkflowComponents/utils.test.ts rename to projects/app/test/pageComponents/app/detail/WorkflowComponents/utils.test.ts diff --git a/test/cases/pages/api/core/dataset/training/updateTrainingData.test.ts b/projects/app/test/pages/api/core/dataset/training/updateTrainingData.test.ts similarity index 100% rename from test/cases/pages/api/core/dataset/training/updateTrainingData.test.ts rename to projects/app/test/pages/api/core/dataset/training/updateTrainingData.test.ts diff --git a/test/cases/pages/api/support/mcp/server/toolList.test.ts b/projects/app/test/pages/api/support/mcp/server/toolList.test.ts similarity index 99% rename from test/cases/pages/api/support/mcp/server/toolList.test.ts rename to projects/app/test/pages/api/support/mcp/server/toolList.test.ts index 2c06c7b18d2b..e3a3610e8465 100644 --- a/test/cases/pages/api/support/mcp/server/toolList.test.ts +++ b/projects/app/test/pages/api/support/mcp/server/toolList.test.ts @@ -63,7 +63,7 @@ describe('toolList', () => { ]; const schema = pluginNodes2InputSchema(nodes); - console.log(schema); + expect(schema).toEqual({ type: 'object', properties: { diff --git a/test/cases/service/support/permission/auth/chat.test.ts b/projects/app/test/service/support/permission/auth/chat.test.ts similarity index 100% rename from test/cases/service/support/permission/auth/chat.test.ts rename to projects/app/test/service/support/permission/auth/chat.test.ts diff --git a/test/cases/service/support/wallet/usage/utils.test.ts b/projects/app/test/service/support/wallet/usage/utils.test.ts similarity index 100% rename from test/cases/service/support/wallet/usage/utils.test.ts rename to projects/app/test/service/support/wallet/usage/utils.test.ts diff --git a/test/cases/web/common/api/request.test.ts b/projects/app/test/web/common/api/request.test.ts similarity index 98% rename from test/cases/web/common/api/request.test.ts rename to projects/app/test/web/common/api/request.test.ts index 49d52fa1665d..2ee693473c23 100644 --- a/test/cases/web/common/api/request.test.ts +++ b/projects/app/test/web/common/api/request.test.ts @@ -5,7 +5,7 @@ import { requestFinish, checkRes, responseError -} from '../../../../../projects/app/src/web/common/api/request'; +} from '../../../../src/web/common/api/request'; import { TeamErrEnum } from '@fastgpt/global/common/error/code/team'; import { TOKEN_ERROR_CODE } from '@fastgpt/global/common/error/errorCode'; diff --git a/test/cases/web/common/utils/eventbus.test.ts b/projects/app/test/web/common/utils/eventbus.test.ts similarity index 100% rename from test/cases/web/common/utils/eventbus.test.ts rename to projects/app/test/web/common/utils/eventbus.test.ts diff --git a/test/cases/web/core/app/utils.test.ts b/projects/app/test/web/core/app/utils.test.ts similarity index 100% rename from test/cases/web/core/app/utils.test.ts rename to projects/app/test/web/core/app/utils.test.ts diff --git a/test/cases/web/core/chat/context/useChatStore.test.ts b/projects/app/test/web/core/chat/context/useChatStore.test.ts similarity index 91% rename from test/cases/web/core/chat/context/useChatStore.test.ts rename to projects/app/test/web/core/chat/context/useChatStore.test.ts index a2125b30b265..53024366f685 100644 --- a/test/cases/web/core/chat/context/useChatStore.test.ts +++ b/projects/app/test/web/core/chat/context/useChatStore.test.ts @@ -106,24 +106,6 @@ describe('useChatStore', () => { expect(useChatStore.getState().chatId).toBe('test'); }); - // SKIP: The test is inconsistent with the current implementation and should be skipped. - it.skip('should restore last chatId as id-part from lastChatId when it is "test-generated-id"', () => { - const store = useChatStore.getState(); - const source = ChatSourceEnum.share; - const chatId = 'test-generated-id'; - - useChatStore.setState({ - lastChatId: `${source}-${chatId}`, - source: undefined, - chatId: '', - lastChatAppId: 'test-app' - }); - - store.setSource(source); - // It should restore chatId to 'test-generated-id' from lastChatId - expect(useChatStore.getState().chatId).toBe('test-generated-id'); - }); - it('should not restore last chat if lastChatId does not match source', () => { const store = useChatStore.getState(); const source = ChatSourceEnum.share; diff --git a/test/cases/web/support/user/api.test.ts b/projects/app/test/web/support/user/api.test.ts similarity index 100% rename from test/cases/web/support/user/api.test.ts rename to projects/app/test/web/support/user/api.test.ts diff --git a/projects/sandbox/src/sandbox/constants.ts b/projects/sandbox/src/sandbox/constants.ts index ad4b74374b1f..900132d6f907 100644 --- a/projects/sandbox/src/sandbox/constants.ts +++ b/projects/sandbox/src/sandbox/constants.ts @@ -18,65 +18,109 @@ def extract_imports(code): for alias in node.names: imports.append(f"from {module} import {alias.name}") return imports + seccomp_prefix = """ -from seccomp import * +import platform import sys -import errno -allowed_syscalls = [ - "syscall.SYS_NEWFSTATAT", - "syscall.SYS_LSEEK", - "syscall.SYS_GETDENTS64", - "syscall.SYS_CLOSE", - "syscall.SYS_FUTEX", - "syscall.SYS_MMAP", - "syscall.SYS_BRK", - "syscall.SYS_MPROTECT", - "syscall.SYS_MUNMAP", - "syscall.SYS_RT_SIGRETURN", - "syscall.SYS_MREMAP", - "syscall.SYS_SETUID", - "syscall.SYS_SETGID", - "syscall.SYS_GETUID", - "syscall.SYS_GETPID", - "syscall.SYS_GETPPID", - "syscall.SYS_GETTID", - "syscall.SYS_EXIT", - "syscall.SYS_EXIT_GROUP", - "syscall.SYS_TGKILL", - "syscall.SYS_RT_SIGACTION", - "syscall.SYS_SCHED_YIELD", - "syscall.SYS_SET_ROBUST_LIST", - "syscall.SYS_GET_ROBUST_LIST", - "syscall.SYS_RSEQ", - "syscall.SYS_CLOCK_GETTIME", - "syscall.SYS_GETTIMEOFDAY", - "syscall.SYS_NANOSLEEP", - "syscall.SYS_CLOCK_NANOSLEEP", - "syscall.SYS_TIME", - "syscall.SYS_RT_SIGPROCMASK", - "syscall.SYS_SIGALTSTACK", - "syscall.SYS_CLONE", - "syscall.SYS_MKDIRAT", - "syscall.SYS_MKDIR", - "syscall.SYS_FSTAT", - "syscall.SYS_FCNTL", - "syscall.SYS_FSTATFS", -] -allowed_syscalls_tmp = allowed_syscalls -L = [] -for item in allowed_syscalls_tmp: - item = item.strip() - parts = item.split(".")[1][4:].lower() - L.append(parts) -f = SyscallFilter(defaction=KILL) -for item in L: - f.add_rule(ALLOW, item) -f.add_rule(ALLOW, "write", Arg(0, EQ, sys.stdout.fileno())) -f.add_rule(ALLOW, "write", Arg(0, EQ, sys.stderr.fileno())) -f.add_rule(ALLOW, 307) -f.add_rule(ALLOW, 318) -f.add_rule(ALLOW, 334) -f.load() + +# Skip seccomp on macOS since it's Linux-specific +if platform.system() == 'Linux': + try: + from seccomp import * + import errno + allowed_syscalls = [ + # File operations - READ ONLY (removed SYS_WRITE) + "syscall.SYS_READ", + # Removed "syscall.SYS_WRITE" - no general write access + "syscall.SYS_OPEN", # Still needed for reading files + "syscall.SYS_OPENAT", # Still needed for reading files + "syscall.SYS_CLOSE", + "syscall.SYS_FSTAT", + "syscall.SYS_LSTAT", + "syscall.SYS_STAT", + "syscall.SYS_NEWFSTATAT", + "syscall.SYS_LSEEK", + "syscall.SYS_GETDENTS64", + "syscall.SYS_FCNTL", + "syscall.SYS_ACCESS", + "syscall.SYS_FACCESSAT", + + # Memory management - essential for Python + "syscall.SYS_MMAP", + "syscall.SYS_BRK", + "syscall.SYS_MPROTECT", + "syscall.SYS_MUNMAP", + "syscall.SYS_MREMAP", + + # Process/thread operations + "syscall.SYS_GETUID", + "syscall.SYS_GETGID", + "syscall.SYS_GETEUID", + "syscall.SYS_GETEGID", + "syscall.SYS_GETPID", + "syscall.SYS_GETPPID", + "syscall.SYS_GETTID", + "syscall.SYS_EXIT", + "syscall.SYS_EXIT_GROUP", + + # Signal handling + "syscall.SYS_RT_SIGACTION", + "syscall.SYS_RT_SIGPROCMASK", + "syscall.SYS_RT_SIGRETURN", + "syscall.SYS_SIGALTSTACK", + + # Time operations + "syscall.SYS_CLOCK_GETTIME", + "syscall.SYS_GETTIMEOFDAY", + "syscall.SYS_TIME", + + # Threading/synchronization + "syscall.SYS_FUTEX", + "syscall.SYS_SET_ROBUST_LIST", + "syscall.SYS_GET_ROBUST_LIST", + "syscall.SYS_CLONE", + + # System info + "syscall.SYS_UNAME", + "syscall.SYS_ARCH_PRCTL", + "syscall.SYS_RSEQ", + + # I/O operations + "syscall.SYS_IOCTL", + "syscall.SYS_POLL", + "syscall.SYS_SELECT", + "syscall.SYS_PSELECT6", + + # Process scheduling + "syscall.SYS_SCHED_YIELD", + "syscall.SYS_SCHED_GETAFFINITY", + + # Additional Python runtime essentials + "syscall.SYS_GETRANDOM", + "syscall.SYS_GETCWD", + "syscall.SYS_READLINK", + "syscall.SYS_READLINKAT", + ] + allowed_syscalls_tmp = allowed_syscalls + L = [] + for item in allowed_syscalls_tmp: + item = item.strip() + parts = item.split(".")[1][4:].lower() + L.append(parts) + f = SyscallFilter(defaction=KILL) + for item in L: + f.add_rule(ALLOW, item) + # Only allow writing to stdout and stderr for output + f.add_rule(ALLOW, "write", Arg(0, EQ, sys.stdout.fileno())) + f.add_rule(ALLOW, "write", Arg(0, EQ, sys.stderr.fileno())) + # Remove other write-related syscalls + # f.add_rule(ALLOW, 307) # Removed - might be file creation + # f.add_rule(ALLOW, 318) # Removed - might be file creation + # f.add_rule(ALLOW, 334) # Removed - might be file creation + f.load() + except ImportError: + # seccomp module not available, skip security restrictions + pass """ def remove_print_statements(code): @@ -96,7 +140,13 @@ def remove_print_statements(code): return ast.unparse(modified_tree) def detect_dangerous_imports(code): - dangerous_modules = ["os", "sys", "subprocess", "shutil", "socket", "ctypes", "multiprocessing", "threading", "pickle"] + # Add file writing modules to the blacklist + dangerous_modules = [ + "os", "sys", "subprocess", "shutil", "socket", "ctypes", + "multiprocessing", "threading", "pickle", + # Additional modules that can write files + "tempfile", "pathlib", "io", "fileinput" + ] tree = ast.parse(code) for node in ast.walk(tree): if isinstance(node, ast.Import): @@ -108,35 +158,104 @@ def detect_dangerous_imports(code): return node.module return None +def detect_file_write_operations(code): + """Detect potential file writing operations in code""" + dangerous_patterns = [ + 'open(', 'file(', 'write(', 'writelines(', + 'with open', 'f.write', '.write(', + 'create', 'mkdir', 'makedirs' + ] + + for pattern in dangerous_patterns: + if pattern in code: + return f"File write operation detected: {pattern}" + return None + def run_pythonCode(data:dict): - if not data or "code" not in data or "variables" not in data: - return {"error": "Invalid request format"} - code = data["code"] + if not data or "code" not in data: + return {"error": "Invalid request format: missing code"} + + code = data.get("code") + if not code or not code.strip(): + return {"error": "Code cannot be empty"} + code = remove_print_statements(code) dangerous_import = detect_dangerous_imports(code) if dangerous_import: return {"error": f"Importing {dangerous_import} is not allowed."} - variables = data["variables"] + + # Check for file write operations + write_operation = detect_file_write_operations(code) + if write_operation: + return {"error": f"File write operations are not allowed: {write_operation}"} + + # Handle variables - default to empty dict if not provided or None + variables = data.get("variables", {}) + if variables is None: + variables = {} + imports = "\\n".join(extract_imports(code)) var_def = "" - output_code = "if __name__ == '__main__':\\n res = main(" + + # Process variables with proper validation for k, v in variables.items(): - one_var = f"{k} = {json.dumps(v)}\\n" - var_def = var_def + one_var - output_code = output_code + k + ", " - if output_code[-1] == "(": - output_code = output_code + ")\\n" - else: - output_code = output_code[:-2] + ")\\n" - output_code = output_code + " print(res)" + if not isinstance(k, str) or not k.strip(): + return {"error": f"Invalid variable name: {repr(k)}"} + + # Use repr() to properly handle Python True/False/None values + try: + one_var = f"{k} = {repr(v)}\\n" + var_def = var_def + one_var + except Exception as e: + return {"error": f"Error processing variable {k}: {str(e)}"} + + # Create a safe main function call with error handling + output_code = '''if __name__ == '__main__': + import inspect + try: + # Get main function signature + sig = inspect.signature(main) + params = list(sig.parameters.keys()) + + # Create arguments dict from available variables + available_vars = {''' + ', '.join([f'"{k}": {k}' for k in variables.keys()]) + '''} + + # Match parameters with available variables + args = [] + kwargs = {} + + for param_name in params: + if param_name in available_vars: + args.append(available_vars[param_name]) + else: + # Check if parameter has default value + param = sig.parameters[param_name] + if param.default is not inspect.Parameter.empty: + break # Stop adding positional args, rest will use defaults + else: + raise TypeError(f"main() missing required argument: '{param_name}'. Available variables: {list(available_vars.keys())}") + + # Call main function + if args: + res = main(*args) + else: + res = main() + + print(res) + except Exception as e: + print({"error": f"Error calling main function: {str(e)}"}) +''' code = imports + "\\n" + seccomp_prefix + "\\n" + var_def + "\\n" + code + "\\n" + output_code + + # Note: We still need to create the subprocess file for execution, + # but user code cannot write additional files tmp_file = os.path.join(data["tempDir"], "subProcess.py") with open(tmp_file, "w", encoding="utf-8") as f: f.write(code) try: result = subprocess.run(["python3", tmp_file], capture_output=True, text=True, timeout=10) if result.returncode == -31: - return {"error": "Dangerous behavior detected."} + return {"error": "Dangerous behavior detected (likely file write attempt)."} if result.stderr != "": return {"error": result.stderr} diff --git a/projects/sandbox/src/sandbox/utils.ts b/projects/sandbox/src/sandbox/utils.ts index be6733aedf06..b0955e6181d6 100644 --- a/projects/sandbox/src/sandbox/utils.ts +++ b/projects/sandbox/src/sandbox/utils.ts @@ -12,7 +12,7 @@ import { createHmac } from './jsFn/crypto'; import { spawn } from 'child_process'; import { pythonScript } from './constants'; const CustomLogStr = 'CUSTOM_LOG'; -const PythonScriptFileName = 'main.py'; + export const runJsSandbox = async ({ code, variables = {} @@ -111,13 +111,31 @@ export const runJsSandbox = async ({ } }; +const PythonScriptFileName = 'main.py'; export const runPythonSandbox = async ({ code, variables = {} }: RunCodeDto): Promise => { + // Validate input parameters + if (!code || typeof code !== 'string' || !code.trim()) { + return Promise.reject('Code cannot be empty'); + } + + // Ensure variables is an object + if (variables === null || variables === undefined) { + variables = {}; + } + if (typeof variables !== 'object' || Array.isArray(variables)) { + return Promise.reject('Variables must be an object'); + } + const tempDir = await mkdtemp(join(tmpdir(), 'python_script_tmp_')); + const dataJson = JSON.stringify({ code, variables, tempDir }); + const dataBase64 = Buffer.from(dataJson).toString('base64'); const mainCallCode = ` -data = ${JSON.stringify({ code, variables, tempDir })} +import json +import base64 +data = json.loads(base64.b64decode('${dataBase64}').decode('utf-8')) res = run_pythonCode(data) print(json.dumps(res)) `; @@ -173,7 +191,6 @@ async function createTempFile(tempFileDirPath: string, context: string) { return { path: tempFilePath, cleanup: () => { - rmSync(tempFilePath); rmSync(tempFileDirPath, { recursive: true, force: true diff --git a/projects/sandbox/test/app.e2e-spec.ts b/projects/sandbox/test/app.e2e-spec.ts deleted file mode 100644 index c3fb506acd23..000000000000 --- a/projects/sandbox/test/app.e2e-spec.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { INestApplication } from '@nestjs/common'; -import * as request from 'supertest'; -import { AppModule } from './../src/app.module'; - -describe('AppController (e2e)', () => { - let app: INestApplication; - - beforeEach(async () => { - const moduleFixture: TestingModule = await Test.createTestingModule({ - imports: [AppModule] - }).compile(); - - app = moduleFixture.createNestApplication(); - await app.init(); - }); - - it('/ (GET)', () => { - return request(app.getHttpServer()).get('/').expect(200).expect('Hello World!'); - }); -}); diff --git a/projects/sandbox/test/jest-e2e.json b/projects/sandbox/test/jest-e2e.json deleted file mode 100644 index e9d912f3e3ce..000000000000 --- a/projects/sandbox/test/jest-e2e.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "moduleFileExtensions": ["js", "json", "ts"], - "rootDir": ".", - "testEnvironment": "node", - "testRegex": ".e2e-spec.ts$", - "transform": { - "^.+\\.(t|j)s$": "ts-jest" - } -} diff --git a/projects/sandbox/test/tsconfig.json b/projects/sandbox/test/tsconfig.json new file mode 100644 index 000000000000..420c417a6bac --- /dev/null +++ b/projects/sandbox/test/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "es2022", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "node", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true, + "baseUrl": "." + }, + "include": ["**/*.test.ts"], + "exclude": ["**/node_modules"] +} diff --git a/test/cases/function/packages/service/core/app/workflow/dispatch/utils.test.ts b/test/cases/function/packages/service/core/app/workflow/dispatch/utils.test.ts deleted file mode 100644 index 3df27e9472a9..000000000000 --- a/test/cases/function/packages/service/core/app/workflow/dispatch/utils.test.ts +++ /dev/null @@ -1,106 +0,0 @@ -import { describe, expect, it } from 'vitest'; -import { getHistories } from '@fastgpt/service/core/workflow/dispatch/utils'; -import { ChatItemValueTypeEnum, ChatRoleEnum } from '@fastgpt/global/core/chat/constants'; -import type { ChatItemType } from '@fastgpt/global/core/chat/type'; - -describe('getHistories test', async () => { - const MockHistories: ChatItemType[] = [ - { - obj: ChatRoleEnum.System, - value: [ - { - type: ChatItemValueTypeEnum.text, - text: { - content: '你好' - } - } - ] - }, - { - obj: ChatRoleEnum.Human, - value: [ - { - type: ChatItemValueTypeEnum.text, - text: { - content: '你好' - } - } - ] - }, - { - obj: ChatRoleEnum.AI, - value: [ - { - type: ChatItemValueTypeEnum.text, - text: { - content: '你好2' - } - } - ] - }, - { - obj: ChatRoleEnum.Human, - value: [ - { - type: ChatItemValueTypeEnum.text, - text: { - content: '你好3' - } - } - ] - }, - { - obj: ChatRoleEnum.AI, - value: [ - { - type: ChatItemValueTypeEnum.text, - text: { - content: '你好4' - } - } - ] - } - ]; - - it('getHistories', async () => { - // Number - expect(getHistories(1, MockHistories)).toEqual([ - ...MockHistories.slice(0, 1), - ...MockHistories.slice(-2) - ]); - expect(getHistories(2, MockHistories)).toEqual([...MockHistories.slice(0)]); - expect(getHistories(4, MockHistories)).toEqual([...MockHistories.slice(0)]); - - // Array - expect( - getHistories( - [ - { - obj: ChatRoleEnum.Human, - value: [ - { - type: ChatItemValueTypeEnum.text, - text: { - content: '你好' - } - } - ] - } - ], - MockHistories - ) - ).toEqual([ - { - obj: ChatRoleEnum.Human, - value: [ - { - type: ChatItemValueTypeEnum.text, - text: { - content: '你好' - } - } - ] - } - ]); - }); -}); diff --git a/test/cases/function/packages/global/common/string/password.test.ts b/test/cases/global/common/string/password.test.ts similarity index 100% rename from test/cases/function/packages/global/common/string/password.test.ts rename to test/cases/global/common/string/password.test.ts diff --git a/test/cases/function/packages/global/common/string/textSplitter.test.ts b/test/cases/global/common/string/textSplitter.test.ts similarity index 100% rename from test/cases/function/packages/global/common/string/textSplitter.test.ts rename to test/cases/global/common/string/textSplitter.test.ts diff --git a/test/cases/function/packages/global/core/dataset/search/utils.test.ts b/test/cases/global/core/dataset/search/utils.test.ts similarity index 100% rename from test/cases/function/packages/global/core/dataset/search/utils.test.ts rename to test/cases/global/core/dataset/search/utils.test.ts diff --git a/test/cases/global/support/permission/common.test.ts b/test/cases/global/support/permission/common.test.ts index b65577662ce3..09d1c437d200 100644 --- a/test/cases/global/support/permission/common.test.ts +++ b/test/cases/global/support/permission/common.test.ts @@ -1,5 +1,10 @@ -import { CommonPerList, CommonRoleList } from '@fastgpt/global/support/permission/constant'; +import { + CommonPerList, + CommonRoleList, + OwnerRoleVal +} from '@fastgpt/global/support/permission/constant'; import { Permission } from '@fastgpt/global/support/permission/controller'; +import type { PermissionValueType } from '@fastgpt/global/support/permission/type'; import { sumPer } from '@fastgpt/global/support/permission/utils'; import { describe, expect, it } from 'vitest'; describe('Permission Helper Class Test', () => { @@ -14,6 +19,22 @@ describe('Permission Helper Class Test', () => { permission.removeRole(CommonRoleList.read.value); expect(permission.checkPer(CommonPerList.manage)).toBe(true); }); + it('Owner Permission Test', () => { + const permission = new Permission({ isOwner: true }); + expect(permission.checkPer(CommonPerList.owner)).toBe(true); + expect(permission.checkPer(CommonPerList.read)).toBe(true); + expect(permission.checkPer(CommonPerList.write)).toBe(true); + expect(permission.checkPer(CommonPerList.manage)).toBe(true); + expect(permission.checkRole(CommonRoleList.read.value)).toBe(true); + expect(permission.checkRole(CommonRoleList.manage.value)).toBe(true); + expect(permission.checkRole(CommonRoleList.write.value)).toBe(true); + expect(permission.checkRole(OwnerRoleVal)).toBe(true); + + permission.addRole(CommonRoleList.read.value); + expect(permission.checkPer(CommonPerList.owner)).toBe(true); + permission.removeRole(CommonRoleList.read.value); + expect(permission.checkPer(CommonPerList.owner)).toBe(true); + }); }); describe('Tool Functions', () => { @@ -22,7 +43,9 @@ describe('Tool Functions', () => { expect(sumPer(0b000, 0b000)).toBe(0b000); expect(sumPer(0b100, 0b001)).toBe(0b101); expect(sumPer(0b111, 0b010)).toBe(0b111); - expect(sumPer(sumPer(0b001, 0b010), 0b100)).toBe(0b111); + expect(sumPer(sumPer(0b001, 0b010) as PermissionValueType, 0b100)).toBe(0b111); expect(sumPer(0b10000000, 0b01000000)).toBe(0b11000000); + expect(sumPer()).toBe(undefined); + expect(sumPer() || 0b111).toBe(0b111); }); }); diff --git a/test/cases/global/support/permission/utils.test.ts b/test/cases/global/support/permission/utils.test.ts new file mode 100644 index 000000000000..816ccd096ff9 --- /dev/null +++ b/test/cases/global/support/permission/utils.test.ts @@ -0,0 +1,112 @@ +import { checkRoleUpdateConflict } from '@fastgpt/global/support/permission/utils'; +import { describe, expect, it } from 'vitest'; + +describe('Test checkRoleUpdateConflict', () => { + it('should return false when no parent collaborators exist', () => { + const result = checkRoleUpdateConflict({ + parentClbs: [], + newChildClbs: [{ permission: 0b001, tmbId: 'fakeTmbId1' }] + }); + expect(result).toBe(false); + }); + + it('should return false when adding new collaborator with different tmbId', () => { + const result = checkRoleUpdateConflict({ + parentClbs: [{ permission: 0b011, tmbId: 'fakeTmbId1' }], + newChildClbs: [{ permission: 0b001, tmbId: 'fakeTmbId2' }] + }); + expect(result).toBe(true); + }); + + it('should return true when changing parent collaborator permission', () => { + const result = checkRoleUpdateConflict({ + parentClbs: [{ permission: 0b011, tmbId: 'fakeTmbId1' }], + newChildClbs: [{ permission: 0b010, tmbId: 'fakeTmbId1' }] + }); + expect(result).toBe(true); + }); + + it('should return false when changed permission bit is not set in parent', () => { + const result = checkRoleUpdateConflict({ + parentClbs: [{ permission: 0b1001, tmbId: 'fakeTmbId1' }], + newChildClbs: [{ permission: 0b1001, tmbId: 'fakeTmbId1' }] + }); + expect(result).toBe(false); + }); + + it('should return false when adding new collaborator alongside existing ones', () => { + const result = checkRoleUpdateConflict({ + parentClbs: [{ permission: 0b1001, tmbId: 'fakeTmbId1' }], + newChildClbs: [ + { permission: 0b1111, tmbId: 'fakeTmbId1' }, + { permission: 0b1001, tmbId: 'fakeTmbId2' } + ] + }); + expect(result).toBe(false); + }); + + it('should return false when adding new collaborator with no existing collaborators', () => { + const result = checkRoleUpdateConflict({ + parentClbs: [], + newChildClbs: [{ permission: 0b1001, tmbId: 'fakeTmbId1' }] + }); + expect(result).toBe(false); + }); + + it('should return false when adding parent collaborator (new collaborator case)', () => { + const result = checkRoleUpdateConflict({ + parentClbs: [{ permission: 0b1001, tmbId: 'fakeTmbId1' }], + newChildClbs: [{ permission: 0b0110, tmbId: 'fakeTmbId1' }] + }); + expect(result).toBe(true); + }); + + it('should return true when deleting parent collaborator', () => { + const result = checkRoleUpdateConflict({ + parentClbs: [{ permission: 0b011, tmbId: 'fakeTmbId1' }], + newChildClbs: [] + }); + expect(result).toBe(true); + }); + + it('should return false when no changes occur', () => { + const result = checkRoleUpdateConflict({ + parentClbs: [{ permission: 0b011, tmbId: 'fakeTmbId1' }], + newChildClbs: [{ permission: 0b011, tmbId: 'fakeTmbId1' }] + }); + expect(result).toBe(false); + }); + + it('should return false when changing permission without conflict', () => { + const result = checkRoleUpdateConflict({ + parentClbs: [{ permission: 0b001, tmbId: 'fakeTmbId1' }], + newChildClbs: [{ permission: 0b011, tmbId: 'fakeTmbId1' }] + }); + expect(result).toBe(false); + }); + + it('should handle multiple parent collaborators correctly', () => { + const result = checkRoleUpdateConflict({ + parentClbs: [ + { permission: 0b011, tmbId: 'parent1' }, + { permission: 0b001, tmbId: 'parent2' } + ], + newChildClbs: [ + { permission: 0b010, tmbId: 'parent1' }, + { permission: 0b001, tmbId: 'parent2' } + ] + }); + expect(result).toBe(true); + }); + + it('should return false when changing non-parent collaborator', () => { + const result = checkRoleUpdateConflict({ + parentClbs: [{ permission: 0b011, tmbId: 'parent1' }], + newChildClbs: [ + { permission: 0b011, tmbId: 'parent1' }, + { permission: 0b010, tmbId: 'child1' } + ] + }); + expect(result).toBe(false); + }); +}); diff --git a/test/cases/function/packages/service/core/ai/parseStreamResponse.test.ts b/test/cases/service/core/ai/parseStreamResponse.test.ts similarity index 100% rename from test/cases/function/packages/service/core/ai/parseStreamResponse.test.ts rename to test/cases/service/core/ai/parseStreamResponse.test.ts diff --git a/test/cases/service/core/app/workflow/dispatch/utils.test.ts b/test/cases/service/core/app/workflow/dispatch/utils.test.ts index 100fa400865e..66e6ccb96822 100644 --- a/test/cases/service/core/app/workflow/dispatch/utils.test.ts +++ b/test/cases/service/core/app/workflow/dispatch/utils.test.ts @@ -311,3 +311,109 @@ describe('valueTypeFormat', () => { // }); // }); }); + +import { getHistories } from '@fastgpt/service/core/workflow/dispatch/utils'; +import { ChatItemValueTypeEnum, ChatRoleEnum } from '@fastgpt/global/core/chat/constants'; +import type { ChatItemType } from '@fastgpt/global/core/chat/type'; + +describe('getHistories test', async () => { + const MockHistories: ChatItemType[] = [ + { + obj: ChatRoleEnum.System, + value: [ + { + type: ChatItemValueTypeEnum.text, + text: { + content: '你好' + } + } + ] + }, + { + obj: ChatRoleEnum.Human, + value: [ + { + type: ChatItemValueTypeEnum.text, + text: { + content: '你好' + } + } + ] + }, + { + obj: ChatRoleEnum.AI, + value: [ + { + type: ChatItemValueTypeEnum.text, + text: { + content: '你好2' + } + } + ] + }, + { + obj: ChatRoleEnum.Human, + value: [ + { + type: ChatItemValueTypeEnum.text, + text: { + content: '你好3' + } + } + ] + }, + { + obj: ChatRoleEnum.AI, + value: [ + { + type: ChatItemValueTypeEnum.text, + text: { + content: '你好4' + } + } + ] + } + ]; + + it('getHistories', async () => { + // Number + expect(getHistories(1, MockHistories)).toEqual([ + ...MockHistories.slice(0, 1), + ...MockHistories.slice(-2) + ]); + expect(getHistories(2, MockHistories)).toEqual([...MockHistories.slice(0)]); + expect(getHistories(4, MockHistories)).toEqual([...MockHistories.slice(0)]); + + // Array + expect( + getHistories( + [ + { + obj: ChatRoleEnum.Human, + value: [ + { + type: ChatItemValueTypeEnum.text, + text: { + content: '你好' + } + } + ] + } + ], + MockHistories + ) + ).toEqual([ + { + obj: ChatRoleEnum.Human, + value: [ + { + type: ChatItemValueTypeEnum.text, + text: { + content: '你好' + } + } + ] + } + ]); + }); +}); diff --git a/test/cases/function/packages/service/core/dataset/textSplitter.test.ts b/test/cases/service/core/dataset/textSplitter.test.ts similarity index 100% rename from test/cases/function/packages/service/core/dataset/textSplitter.test.ts rename to test/cases/service/core/dataset/textSplitter.test.ts diff --git a/test/cases/service/support/mcp/utils.test.ts b/test/cases/service/support/mcp/utils.test.ts index e9ad48a8212c..9d1fb8b50f09 100644 --- a/test/cases/service/support/mcp/utils.test.ts +++ b/test/cases/service/support/mcp/utils.test.ts @@ -2,8 +2,7 @@ import { describe, expect, it, vi } from 'vitest'; import { pluginNodes2InputSchema, workflow2InputSchema, - getMcpServerTools, - callMcpServerTool + getMcpServerTools } from '@/service/support/mcp/utils'; import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant'; import { MongoMcpKey } from '@fastgpt/service/support/mcp/schema'; @@ -12,11 +11,6 @@ import { AppTypeEnum } from '@fastgpt/global/core/app/constants'; import { CommonErrEnum } from '@fastgpt/global/common/error/code/common'; import { authAppByTmbId } from '@fastgpt/service/support/permission/app/auth'; import { getAppLatestVersion } from '@fastgpt/service/core/app/version/controller'; -import { - getUserChatInfoAndAuthTeamPoints, - getRunningUserInfoByTmbId -} from '@fastgpt/service/support/permission/auth/team'; -import { dispatchWorkFlow } from '@fastgpt/service/core/workflow/dispatch'; vi.mock('@fastgpt/service/support/mcp/schema', () => ({ MongoMcpKey: { diff --git a/test/cases/service/support/permission/controller.test.ts b/test/cases/service/support/permission/controller.test.ts new file mode 100644 index 000000000000..dfd5dbcf3b91 --- /dev/null +++ b/test/cases/service/support/permission/controller.test.ts @@ -0,0 +1,128 @@ +import type { CreateAppBody } from '@/pages/api/core/app/create'; +import createAppAPI from '@/pages/api/core/app/create'; +import { DEFAULT_ORG_AVATAR, DEFAULT_TEAM_AVATAR } from '@fastgpt/global/common/system/constants'; +import { AppTypeEnum } from '@fastgpt/global/core/app/constants'; +import { OwnerRoleVal, PerResourceTypeEnum } from '@fastgpt/global/support/permission/constant'; +import { getClbsInfo, getResourceOwnedClbs } from '@fastgpt/service/support/permission/controller'; +import { MongoMemberGroupModel } from '@fastgpt/service/support/permission/memberGroup/memberGroupSchema'; +import { MongoOrgModel } from '@fastgpt/service/support/permission/org/orgSchema'; +import { MongoResourcePermission } from '@fastgpt/service/support/permission/schema'; +import { getFakeGroups, getFakeOrgs, getFakeUsers } from '@test/datas/users'; +import { Call } from '@test/utils/request'; +import { describe, expect, it } from 'vitest'; + +describe('test getClbsWithInfo', () => { + it('should get ClbsWithInfo', async () => { + // tmb, group, avatar + // get name, avatar, default avatar fallback + const users = await getFakeUsers(3); + const orgs = await getFakeOrgs(); + const groups = await getFakeGroups(3); + const app = await Call(createAppAPI, { + auth: users.owner, + body: { + modules: [], + name: 'test', + type: AppTypeEnum.simple + } + }); + + expect(app.data).toBeDefined(); + + await MongoResourcePermission.create( + users.members.map((member) => ({ + resourceId: app.data, + permission: 4, + resourceType: 'app', + teamId: member.teamId, + tmbId: member.tmbId + })) + ); + + await MongoMemberGroupModel.updateOne( + { + _id: groups[0]._id + }, + { + avatar: 'test avatar' + } + ); + + await MongoOrgModel.updateOne( + { + _id: orgs[0]._id + }, + { + avatar: 'test avatar' + } + ); + + await MongoResourcePermission.create( + groups.map((group) => ({ + resourceId: app.data, + permission: 4, + resourceType: 'app', + teamId: group.teamId, + groupId: group._id + })) + ); + + await MongoResourcePermission.create( + orgs.map((org) => ({ + resourceId: app.data, + permission: 4, + resourceType: 'app', + teamId: org.teamId, + orgId: org._id + })) + ); + + const clbs = await getResourceOwnedClbs({ + resourceType: PerResourceTypeEnum.app, + resourceId: String(app.data), + teamId: users.manager.teamId + }); + + expect(clbs.length).eq(13); // 3 users, 3 groups, 6 orgs, 1 owner + expect(clbs.filter((clb) => !!clb.tmbId).length).eq(4); + expect(clbs.filter((clb) => !!clb.groupId).length).eq(3); + expect(clbs.filter((clb) => !!clb.orgId).length).eq(6); + + const clbWithInfos = await getClbsInfo({ + clbs, + teamId: users.manager.teamId, + ownerTmbId: users.owner.tmbId + }); + + expect(clbWithInfos.length).eq(13); + expect(clbWithInfos.filter((clb) => !!clb.tmbId).length).eq(4); + expect(clbWithInfos.filter((clb) => !!clb.groupId).length).eq(3); + expect(clbWithInfos.filter((clb) => !!clb.orgId).length).eq(6); + + expect(clbWithInfos.map((clb) => clb.name).toSorted()).to.deep.equal( + [ + 'Member', + 'Member', + 'Member', + 'Owner', + 'group1', + 'group2', + 'group3', + 'org1', + 'org2', + 'org3', + 'org4', + 'org5', + 'root' + ].toSorted() + ); + + expect(clbWithInfos.filter((clb) => clb.avatar === DEFAULT_ORG_AVATAR).length).eq(5); + expect(clbWithInfos.filter((clb) => clb.avatar === DEFAULT_TEAM_AVATAR).length).eq(2); + expect(clbWithInfos.filter((clb) => clb.avatar === 'test avatar').length).eq(2); + + expect(clbWithInfos.map((clb) => clb.permission.role).toSorted()).deep.equal( + [...Array.from({ length: 12 }, () => 4), OwnerRoleVal].toSorted() + ); + }); +}); diff --git a/test/cases/service/support/permission/inheritPermission.test.ts b/test/cases/service/support/permission/inheritPermission.test.ts new file mode 100644 index 000000000000..85ef937382e7 --- /dev/null +++ b/test/cases/service/support/permission/inheritPermission.test.ts @@ -0,0 +1,246 @@ +import { AppFolderTypeList, AppTypeEnum } from '@fastgpt/global/core/app/constants'; +import { + ManageRoleVal, + OwnerRoleVal, + PerResourceTypeEnum, + ReadRoleVal +} from '@fastgpt/global/support/permission/constant'; +import { mongoSessionRun } from '@fastgpt/service/common/mongo/sessionRun'; +import { MongoApp } from '@fastgpt/service/core/app/schema'; +import { createResourceDefaultCollaborators } from '@fastgpt/service/support/permission/controller'; +import { syncChildrenPermission } from '@fastgpt/service/support/permission/inheritPermission'; +import { MongoResourcePermission } from '@fastgpt/service/support/permission/schema'; +import { getFakeUsers } from '@test/datas/users'; +import type { parseHeaderCertRet } from '@test/mocks/request'; +import { describe, it, expect } from 'vitest'; + +describe('syncChildrenPermission', () => { + const createApp = async ({ + user, + name, + type, + parentId + }: { + user: parseHeaderCertRet; + name: string; + type: AppTypeEnum; + parentId?: string; + }) => + mongoSessionRun(async (session) => { + const app = await MongoApp.create({ + teamId: user.teamId, + tmbId: user.tmbId, + ...(parentId ? { parentId } : {}), + name, + type, + inheritPermission: true + }); + if (type === 'folder') { + await createResourceDefaultCollaborators({ + resource: app, + resourceType: PerResourceTypeEnum.app, + session, + tmbId: String(user.tmbId) + }); + } + return app; + }); + + it('sync: add/update/delete clbs', async () => { + const users = await getFakeUsers(5); + const f1 = await createApp({ + user: users.owner, + name: 'f1', + type: AppTypeEnum.folder + }); + const f2 = await createApp({ + user: users.owner, + name: 'f2', + type: AppTypeEnum.folder, + parentId: String(f1._id) + }); + expect( + await MongoResourcePermission.countDocuments({ + resourceType: 'app' + }) + ).eq(2); + const clbs = [ + { + tmbId: String(users.owner.tmbId), + permission: OwnerRoleVal + }, + { + tmbId: String(users.members[0].tmbId), + permission: ReadRoleVal + }, + { + tmbId: users.members[1].tmbId, + permission: ReadRoleVal + } + ]; + + await mongoSessionRun(async (session) => { + await syncChildrenPermission({ + collaborators: clbs, + folderTypeList: AppFolderTypeList, + resource: f1, + resourceModel: MongoApp, + resourceType: PerResourceTypeEnum.app, + session + }); + await MongoResourcePermission.insertOne({ + resourceId: f1._id, + resourceType: PerResourceTypeEnum.app, + permission: ReadRoleVal, + tmbId: users.members[0].tmbId, + teamId: users.members[0].teamId, + session + }); + await MongoResourcePermission.insertOne({ + resourceId: f1._id, + resourceType: PerResourceTypeEnum.app, + permission: ReadRoleVal, + tmbId: users.members[1].tmbId, + teamId: users.members[1].teamId, + session + }); + }); + + expect( + await MongoResourcePermission.countDocuments({ + resourceType: 'app' + }) + ).eq(6); + + const f3 = await createApp({ + name: 'f3', + user: users.owner, + type: AppTypeEnum.folder, + parentId: String(f2._id) + }); + + await mongoSessionRun(async (session) => { + await syncChildrenPermission({ + collaborators: clbs, + folderTypeList: AppFolderTypeList, + resource: f3, + resourceModel: MongoApp, + resourceType: PerResourceTypeEnum.app, + session + }); + }); + + expect( + await MongoResourcePermission.countDocuments({ + resourceType: 'app' + }) + ).eq(9); + + const a1 = await createApp({ + name: 'a1', + user: users.owner, + type: AppTypeEnum.simple, + parentId: String(f3._id) + }); + + await mongoSessionRun(async (session) => { + await syncChildrenPermission({ + collaborators: clbs, + folderTypeList: AppFolderTypeList, + resource: a1, + resourceModel: MongoApp, + resourceType: PerResourceTypeEnum.app, + session + }); + }); + + expect( + await MongoResourcePermission.countDocuments({ + resourceType: 'app' + }) + ).eq(9); + + // update + await mongoSessionRun(async (session) => { + const clbs = [ + { + tmbId: String(users.owner.tmbId), + permission: OwnerRoleVal + }, + { + tmbId: String(users.members[0].tmbId), + permission: ReadRoleVal + }, + { + tmbId: String(users.members[1].tmbId), + permission: ManageRoleVal + } + ]; + await syncChildrenPermission({ + collaborators: clbs, + folderTypeList: AppFolderTypeList, + resource: f1, + resourceModel: MongoApp, + resourceType: PerResourceTypeEnum.app, + session + }); + + await MongoResourcePermission.updateOne( + { + resourceType: PerResourceTypeEnum.app, + resourceId: String(f1._id), + tmbId: String(users.members[1].tmbId) + }, + { + permission: ManageRoleVal + } + ); + }); + + // console.log(await MongoResourcePermission.find({ resourceType: 'app' })); + + expect( + await MongoResourcePermission.countDocuments({ + resourceType: 'app' + }) + ).eq(9); + + // delete + await mongoSessionRun(async (session) => { + const clbs = [ + { + tmbId: String(users.owner.tmbId), + permission: OwnerRoleVal + }, + { + tmbId: String(users.members[0].tmbId), + permission: ReadRoleVal + } + ]; + await syncChildrenPermission({ + collaborators: clbs, + folderTypeList: AppFolderTypeList, + resource: f1, + resourceModel: MongoApp, + resourceType: PerResourceTypeEnum.app, + session + }); + + await MongoResourcePermission.deleteOne( + { + resourceType: PerResourceTypeEnum.app, + resourceId: String(f1._id), + tmbId: String(users.members[1].tmbId), + team: String(users.members[1].teamId) + }, + { session } + ); + }); + + expect( + await MongoResourcePermission.countDocuments({ + resourceType: 'app' + }) + ).eq(8); + }); +}); diff --git a/test/datas/users.ts b/test/datas/users.ts index 1358b832cd78..4941a98ad350 100644 --- a/test/datas/users.ts +++ b/test/datas/users.ts @@ -11,6 +11,7 @@ import { MongoResourcePermission } from '@fastgpt/service/support/permission/sch import { MongoUser } from '@fastgpt/service/support/user/schema'; import { MongoTeamMember } from '@fastgpt/service/support/user/team/teamMemberSchema'; import { MongoTeam } from '@fastgpt/service/support/user/team/teamSchema'; +import { initTeamFreePlan } from '@fastgpt/service/support/wallet/sub/utils'; import type { parseHeaderCertRet } from 'test/mocks/request'; export async function getRootUser(): Promise { @@ -24,6 +25,11 @@ export async function getRootUser(): Promise { ownerId: rootUser._id }); + // Initialize free subscription plan for the team + await initTeamFreePlan({ + teamId: String(team._id) + }); + const tmb = await MongoTeamMember.create({ teamId: team._id, userId: rootUser._id, @@ -38,7 +44,8 @@ export async function getRootUser(): Promise { isRoot: true, sourceName: undefined, teamId: tmb?.teamId, - tmbId: tmb?._id + tmbId: tmb?._id, + sessionId: '' }; } @@ -54,6 +61,12 @@ export async function getUser(username: string, teamId?: string): Promise { - const actual = await importOriginal(); +vi.mock('@fastgpt/service/support/audit/util', async (importOriginal) => { + const actual = (await importOriginal()) as any; return { ...actual, addAuditLog: vi.fn() }; }); + +// Mock Redis connections to prevent connection errors in tests +vi.mock('@fastgpt/service/common/redis', async (importOriginal) => { + const actual = (await importOriginal()) as any; + + // Create a mock Redis client + const mockRedisClient = { + on: vi.fn(), + connect: vi.fn().mockResolvedValue(undefined), + disconnect: vi.fn().mockResolvedValue(undefined), + keys: vi.fn().mockResolvedValue([]), + get: vi.fn().mockResolvedValue(null), + set: vi.fn().mockResolvedValue('OK'), + del: vi.fn().mockResolvedValue(1), + exists: vi.fn().mockResolvedValue(0), + expire: vi.fn().mockResolvedValue(1), + ttl: vi.fn().mockResolvedValue(-1) + }; + + return { + ...actual, + newQueueRedisConnection: vi.fn(() => mockRedisClient), + newWorkerRedisConnection: vi.fn(() => mockRedisClient), + getGlobalRedisConnection: vi.fn(() => mockRedisClient) + }; +}); + +// Mock BullMQ to prevent queue connection errors +vi.mock('@fastgpt/service/common/bullmq', async (importOriginal) => { + const actual = (await importOriginal()) as any; + + const mockQueue = { + add: vi.fn().mockResolvedValue({ id: '1' }), + close: vi.fn().mockResolvedValue(undefined), + on: vi.fn() + }; + + const mockWorker = { + close: vi.fn().mockResolvedValue(undefined), + on: vi.fn() + }; + + return { + ...actual, + getQueue: vi.fn(() => mockQueue), + getWorker: vi.fn(() => mockWorker) + }; +}); diff --git a/test/mocks/request.ts b/test/mocks/request.ts index 564b2e3d6d26..09574c203926 100644 --- a/test/mocks/request.ts +++ b/test/mocks/request.ts @@ -56,6 +56,7 @@ export type parseHeaderCertRet = { sourceName: string | undefined; apikey: string; isRoot: boolean; + sessionId: string; }; export type MockReqType = { @@ -66,7 +67,7 @@ export type MockReqType = { [key: string]: any; }; -vi.mock(import('@fastgpt/service/support/permission/controller'), async (importOriginal) => { +vi.mock(import('@fastgpt/service/support/permission/auth/common'), async (importOriginal) => { const mod = await importOriginal(); const parseHeaderCert = vi.fn( ({ @@ -87,9 +88,20 @@ vi.mock(import('@fastgpt/service/support/permission/controller'), async (importO return Promise.resolve(auth); } ); + + const authCert = async (props: any) => { + const result = await parseHeaderCert(props); + + return { + ...result, + isOwner: true, + canWrite: true + }; + }; return { ...mod, - parseHeaderCert + parseHeaderCert, + authCert }; }); diff --git a/test/setup.ts b/test/setup.ts index d3793afde633..b58b1e90e97a 100644 --- a/test/setup.ts +++ b/test/setup.ts @@ -31,7 +31,7 @@ vi.mock(import('@/service/common/system'), async (importOriginal) => { return '0.0.0'; }, readConfigData: async () => { - return readFileSync('@/data/config.json', 'utf-8'); + return readFileSync('projects/app/data/config.json', 'utf-8'); }, initSystemConfig: async () => { // read env from projects/app/.env diff --git a/vitest.config.mts b/vitest.config.mts index 42e6098b3814..74d67e6fa814 100644 --- a/vitest.config.mts +++ b/vitest.config.mts @@ -1,6 +1,14 @@ import { resolve } from 'path'; import { defineConfig } from 'vitest/config'; + export default defineConfig({ + resolve: { + alias: { + '@': resolve('projects/app/src'), + '@fastgpt': resolve('packages'), + '@test': resolve('test') + } + }, test: { coverage: { enabled: true, @@ -15,15 +23,13 @@ export default defineConfig({ // fileParallelism: false, maxConcurrency: 5, pool: 'threads', - include: ['test/test.ts', 'test/cases/**/*.test.ts', 'projects/app/test/**/*.test.ts'], + include: [ + 'test/test.ts', + 'test/cases/**/*.test.ts', + 'projects/app/test/**/*.test.ts', + 'projects/sandbox/test/**/*.test.ts' + ], testTimeout: 20000, reporters: ['github-actions', 'default'] - }, - resolve: { - alias: { - '@': resolve('projects/app/src'), - '@fastgpt': resolve('packages'), - '@test': resolve('test') - } } });