diff --git a/document/content/docs/introduction/development/openapi/dataset.mdx b/document/content/docs/introduction/development/openapi/dataset.mdx index 46df17a35..9dd99acac 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 5e1659ad9..a49c20408 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 000000000..7ef63c0ec --- /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 72d8005d2..30c62716d 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 2de5862b2..996ab6946 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 000000000..b68d8d988 --- /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 c93efb846..0dc2b17e6 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 000000000..1be632820 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 000000000..6f2ee2be8 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 000000000..5b49a5bc4 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 000000000..136ce94bb 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 000000000..feb7668c2 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 000000000..0a956f2ad 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 000000000..a67f2f1ec 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 000000000..e3f06ab33 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 000000000..c886e3557 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 000000000..249144a06 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 000000000..b987d4cc0 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 000000000..ab5bc4420 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 000000000..fb32ff4ea 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 000000000..0a61396fe 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 000000000..7f5bb961d 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 000000000..ba8bab218 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 000000000..c16439789 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 000000000..65f0764b5 Binary files /dev/null and b/document/public/imgs/wecom-bot-9.png differ diff --git a/package.json b/package.json index c6107570b..ec178620f 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 c52feedb4..fa5bc8a7f 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 575d9fc47..71279c0ad 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 98261ede6..75e16b2f6 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 150864c5d..6e927c812 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 DeletePermissionQuery = RequireOnlyOne<{ - tmbId?: string; - groupId?: string; - orgId?: string; -}>; +export type CollaboratorItemType = { + permission: PermissionValueType; +} & CollaboratorIdType; + +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 db0a085e6..c1752210a 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 9a99d873b..890f50d79 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 b39900f06..c4249487a 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 f1b79ae8a..9cec7c44c 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 306edac32..d16c9c59e 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 0abb1e615..5666d6057 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 294bfdd47..ac249cafa 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 95fe68c86..bcd496a6c 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 5d2f58db6..350a02847 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 185661657..41b84ced2 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 303b20e28..5cf05f1fb 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 632c84e1c..09e466ade 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 ff3628b6f..ca597eae4 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 f4523040c..bb146d44b 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 0accefdc7..0f1e42466 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 }) - }; - } + const isGetParentClb = + app.inheritPermission && !AppFolderTypeList.includes(app.type) && !!app.parentId; - 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 }); + 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 + }) + ]); - 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 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 31b0b13cd..23f40362a 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 6ea324d04..bc4d86807 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 206eff13d..a2a404fce 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 96279c472..fc08b751f 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 +export const getClbsInfo = async ({ + clbs, + teamId, + ownerTmbId }: { + clbs: CollaboratorItemType[]; 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() - ]); + ownerTmbId?: string; +}): Promise => { + const tmbIds = []; + const orgIds = []; + const groupIds = []; -export const delResourcePermissionById = (id: string) => { - return MongoResourcePermission.findByIdAndDelete(id); -}; -export const delResourcePermission = ({ - session, - tmbId, - groupId, - orgId, - ...props -}: { - resourceType: PerResourceTypeEnum; - 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); + 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); } - return MongoResourcePermission.deleteOne( - { - ...(tmbId ? { tmbId } : {}), - ...(groupId ? { groupId } : {}), - ...(orgId ? { orgId } : {}), - ...props - }, - { session } - ); -}; + 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(); -/* 下面代码等迁移 */ - -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 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); - - 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 - }); - }); }); +}; + +export const createResourceDefaultCollaborators = async ({ + resource, + resourceType, + session, + tmbId +}: { + resource: SyncChildrenPermissionResourceType; + resourceType: PerResourceTypeEnum; + + // 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; + }), + { + tmbId, + permission: OwnerRoleVal + } + ]; + + 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 + } + }); + } + + await MongoResourcePermission.bulkWrite(ops, { session }); +}; diff --git a/packages/service/support/permission/dataset/auth.ts b/packages/service/support/permission/dataset/auth.ts index 3b4ca9d83..faa53dd13 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 [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 + }) + ]); - const Per = new DatasetPermission({ - role: parent.permission.role, - isOwner - }); - - 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 1622ef31e..c5d4266a3 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 2ee7d9371..c705f2263 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 queue = [String(resource._id)]; - const children: string[] = []; - 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 allClbs = await MongoResourcePermission.find({ + resourceType, + teamId, + resourceId: { + $in: allFolders.map((folder) => folder._id) + } + }) + .lean() + .session(session); - // 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 - }); + /** 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 ops: AnyBulkWriteOperation[] = []; + const latestClbMap = new Map(latestClbList.map((clb) => [getCollaboratorId(clb), { ...clb }])); + + while (queue.length) { + 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))); + + // 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,9 +214,54 @@ 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 parentOwner = parentClbs.find((clb) => clb.permission === OwnerRoleVal); + + 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) => { - // update the resource permission + if (isFolder) { + // sync self + await syncCollaborators({ + resourceType, + collaborators, + teamId: resource.teamId, + resourceId: resource._id, + session + }); + // sync children + await syncChildrenPermission({ + resource, + resourceModel, + folderTypeList, + resourceType, + session, + collaborators + }); + } + await resourceModel.updateOne( { _id: resource._id @@ -120,39 +271,6 @@ export async function resumeInheritPermission({ }, { session } ); - - // Folder resource, need to sync children - if (isFolder) { - const parentClbsAndGroups = await getResourceClbsAndGroups({ - resourceId: resource.parentId, - teamId: resource.teamId, - resourceType, - session - }); - - // sync self - await syncCollaborators({ - resourceType, - collaborators: parentClbsAndGroups, - teamId: resource.teamId, - resourceId: resource._id, - session - }); - // sync children - await syncChildrenPermission({ - resource: { - ...resource - }, - resourceModel, - folderTypeList, - resourceType, - session, - collaborators: parentClbsAndGroups - }); - } else { - // Not folder, delete all clb - await MongoResourcePermission.deleteMany({ resourceId: resource._id }, { 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 3d775e11c..3620a8b24 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 94fa531e4..54acb4820 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 269ce4854..479c3babb 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 ae53c7da1..56c5e602d 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 5029008fc..88dd38555 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 000000000..eb4a70b80 --- /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 a399bd358..fa8ed440b 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 95b19e0ea..6006d959e 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 4f4a0433c..f11a8ff34 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 a4359493b..032ca9d2b 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 66cf23916..d62f4b738 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 e3603800e..44cc28452 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 1e70eff6f..e8f5f87e1 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 b34f9b40d..36b54af9e 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 df7f700ed..8e682bfce 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 eba6175fc..05ad96199 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 c30dc82bf..88e091a9b 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 5dae70096..6b488c9d2 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 15b7598be..67f3c37bd 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 a8fa488fc..cea7ed34d 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 45936b40a..f79683ad6 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 aa3cb957a..08f17c3af 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 09e5b466b..62039b19b 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 1022a98c2..b5268075b 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 012e784ad..25b9a1c8b 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 795af0589..98262cfc2 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 0b53de95f..7c134ee46 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 0d951ee6a..c08cb9a43 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 76ad3df5a..72758b9d8 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 5b2cf74ed..a2ca56d40 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 6e2d13d21..ea918ac1c 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 1e1abf15e..cc3a31fa1 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 b27658c2b..26fe8b21e 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 fb38039f3..b560da9fe 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 559381e96..9d49ff1b7 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 e4908f1dc..21eb39850 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 cc184b095..62e21b87e 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 8c7e03372..c30c20d49 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 bd5ae5434..e0907cda9 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 cf84645e7..000000000 --- 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 5811fd7a6..4c9826114 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 3755d237e..514021d2e 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)} + + {/* 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 MemberList = ( + + ); + return searchKey ? ( + {MemberList} + ) : ( + - - - {item.label} - - - + {MemberList} + ); - })} - - )} + })()} + {(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 ( + + ); + })} + + )} + + - {/* 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); - } - return [...state, member]; - }); - }; - const collaborator = collaboratorList?.find((v) => v.tmbId === member.tmbId); - return ( - v.tmbId === member.tmbId)} - orgs={member.orgs} - /> - ); - }); - 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 ? ( - 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]; + + {`${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 6d4e29700..150eea3f3 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 fd50190ee..6396d0b5d 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 18b3aadc5..5dd70f341 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 cb2ea00ae..7ecf3c082 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 e638aea48..d81f06b98 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 bdb78f0d9..b12c73666 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 3174790ca..8870552ff 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 83e193115..76b5ddf94 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 c1ca2cd69..b5e5dbca3 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 fa944f022..cf7ad7fd4 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 43980ef42..72591457b 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 26d0c436f..18621afbf 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 5db5fe771..39cb937c9 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 62c4687a7..c357e097a 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 43b9cc323..be1b3d428 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 e2e367bfe..3f0a7f3f0 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 1e8d95ac3..b49e76c30 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 19a0d2df0..ea96fc073 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 064a63220..15aabe15c 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 8fc108053..eb37f294c 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 000000000..5f0ddb600 --- /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 0f84b5e95..c81a4b0de 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 ba715fbe9..fbd9d20b5 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 63340f784..e0845a2b9 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 bcfdb9f58..cad649862 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 88ff0d7e9..b986a7b19 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 17839ef12..f5c33a57a 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 a58f8f86f..5709f8ccc 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 003ca6f44..36488944f 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 f510b18ea..0dfbc7114 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 19935b3c2..ed510848a 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 76b9abfd2..0e54817a2 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 71ec9b53a..482d3f623 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 3bb34ad22..426db0a7f 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 7fce507c0..e1cec5361 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 0d679c4a0..a2b0611ad 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 c54e6a620..a2d35c8cb 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 0b46aba8e..fd2b6e89a 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 deaa82583..d2b11d9fa 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 423c5a49b..5e86d3c66 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 19052a302..f0b1da3f7 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 3079ed7df..c6ebbcd62 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 4b8b5759e..dedb5f17f 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 2d7ba601d..d89c8bf95 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 fda0564c3..27b6ff33c 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 bee2fb7ef..d1ec54eec 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 6c69d73b5..6a8336805 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 5231f1204..5a07ebda0 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 9a2975e83..aba4f93fe 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 0e470a1d9..69ae40d71 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 d4a0b2793..2c3f672bc 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 2c06c7b18..e3a3610e8 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 49d52fa16..2ee693473 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 a2125b30b..53024366f 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 ad4b74374..900132d6f 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 be6733aed..b0955e618 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 c3fb506ac..000000000 --- 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 e9d912f3e..000000000 --- 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 000000000..420c417a6 --- /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 3df27e947..000000000 --- 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 b65577662..09d1c437d 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 000000000..816ccd096 --- /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 100fa4008..66e6ccb96 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 e9ad48a82..9d1fb8b50 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 000000000..dfd5dbcf3 --- /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 000000000..85ef93738 --- /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 1358b832c..4941a98ad 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 564b2e3d6..09574c203 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 d3793afde..b58b1e90e 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 42e6098b3..74d67e6fa 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') - } } });