V4.12.4 features (#5626)

* fix: push again, user select option button and form input radio content overflow (#5601)

* fix: push again, user select option button and form input radio content overflow

* fix: use useCallback instead of useMemo, fix unnecessary delete

* fix: Move the variable inside the component

* fix: do not pass valueLabel to MySelect

* ui

* del collection api adapt

* refactor: inherit permission (#5529)

* refactor: permission update conflict check function

* refactor(permission): app collaborator update api

* refactor(permission): support app update collaborator

* feat: support fe permission conflict check

* refactor(permission): app permission

* refactor(permission): dataset permission

* refactor(permission): team permission

* chore: fe adjust

* fix: type error

* fix: audit pagiation

* fix: tc

* chore: initv4130

* fix: app/dataset auth logic

* chore: move code

* refactor(permission): remove selfPermission

* fix: mock

* fix: test

* fix: app & dataset auth

* fix: inherit

* test(inheritPermission): test syncChildrenPermission

* prompt editor add list plugin (#5620)

* perf: search result (#5608)

* fix: table size (#5598)

* temp: list value

* backspace

* optimize code

---------

Co-authored-by: Archer <545436317@qq.com>
Co-authored-by: 伍闲犬 <whoeverimf5@gmail.com>

* fix: fe & member list (#5619)

* chore: initv4130

* fix: MemberItemCard

* fix: MemberItemCard

* chore: fe adjust & init script

* perf: test code

* doc

* fix debug variables (#5617)

* perf: search result (#5608)

* fix: table size (#5598)

* fix debug variables

* fix

---------

Co-authored-by: Archer <545436317@qq.com>
Co-authored-by: 伍闲犬 <whoeverimf5@gmail.com>

* perf: member ui

* fix: inherit bug (#5624)

* refactor(permission): remove getClbsWithInfo, which is useless

* fix: app list privateApp

* fix: get infos

* perf(fe): remove delete icon when it is disable in MemberItemCard

* fix: dataset private dataset

* Apply suggestion from @Copilot

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Apply suggestion from @Copilot

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Archer <545436317@qq.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* perf: auto coupon

* chore: upgrade script & get infos avatar  (#5625)

* fix: get infos

* chore: initv4130

* feat: support WecomRobot publish, and fix AesKey can not save bug (#5526)

* feat: resolve conflicts

* fix: add param 'show_publish_wecom'

* feat: abstract out WecomCrypto type

* doc: wecom robot document

* fix: solve instability in AI output

* doc: update some pictures

* feat: remove functions from request.ts to chat.ts and toolCall.ts

* doc: wecom robot doc update

* fix

* delete unused code

* doc: update version and prompt

* feat: remove wecom crypto, delete wecom code in workflow

* feat: delete unused codes

---------

Co-authored-by: heheer <zhiyu44@qq.com>

* remove test

* rename init shell

* feat: collection page store

* reload sandbox

* pysandbox

* remove log

* chore: remove useless code (#5629)

* chore: remove useless code

* fix: checkConflict

* perf: support hidden type for RoleList

* fix: copy node

* update doc

* fix(permission): some bug (#5632)

* fix: app/dataset list

* fix: inherit bug

* perf: del app;i18n;save chat

* fix: test

* i18n

* fix: sumper overflow return OwnerRoleVal (#5633)

* remove invalid code

* fix: scroll

* fix: objectId

* update next

* update package

* object id

* mock redis

* feat: add redis append to resolve wecom stream response  (#5643)

* feat: resolve conflicts

* fix: add param 'show_publish_wecom'

* feat: abstract out WecomCrypto type

* doc: wecom robot document

* fix: solve instability in AI output

* doc: update some pictures

* feat: remove functions from request.ts to chat.ts and toolCall.ts

* doc: wecom robot doc update

* fix

* delete unused code

* doc: update version and prompt

* feat: remove wecom crypto, delete wecom code in workflow

* feat: delete unused codes

* feat: add redis append method

---------

Co-authored-by: heheer <zhiyu44@qq.com>

* cache per

* fix(test): init team sub when creating mocked user (#5646)

* fix: button is not vertically centered (#5647)

* doc

* fix: gridFs objectId (#5649)

---------

Co-authored-by: Zeng Qingwen <143274079+fishwww-ww@users.noreply.github.com>
Co-authored-by: Finley Ge <32237950+FinleyGe@users.noreply.github.com>
Co-authored-by: heheer <heheer@sealos.io>
Co-authored-by: 伍闲犬 <whoeverimf5@gmail.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: heheer <zhiyu44@qq.com>
This commit is contained in:
Archer 2025-09-15 20:02:54 +08:00 committed by GitHub
parent c8934e3d22
commit 2ed1545eb5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
187 changed files with 3701 additions and 2221 deletions

View File

@ -868,8 +868,12 @@ curl --location --request PUT 'http://localhost:3000/api/core/dataset/collection
<Tab value="请求示例" >
```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"]
}'
```
</Tab>
@ -877,7 +881,7 @@ curl --location --request DELETE 'http://localhost:3000/api/core/dataset/collect
<Tab value="参数说明" >
<div>
- id: 集合的ID
- collectionIds: 集合的 ID 列表
</div>
</Tab>

View File

@ -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)

View File

@ -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 模式下,全局变量未传递。
## 🔨 插件更新

View File

@ -1,5 +1,5 @@
{
"title": "4.12.x",
"description": "",
"pages": ["4123", "4122", "4121", "4120"]
"pages": ["4124", "4123", "4122", "4121", "4120"]
}

View File

@ -1,5 +1,5 @@
{
"title": "外部调用 FastGPT",
"description": "外部应用通过多种方式调用 FastGPT 功能的教程",
"pages": ["openapi", "feishu", "dingtalk", "official_account"]
"pages": ["openapi", "feishu", "dingtalk", "wecom", "official_account"]
}

View File

@ -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. 如果没记录,则可能是应用运行报错了,可以先试试最简单的机器人。(飞书机器人无法输入全局变量、文件、图片内容)

View File

@ -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"
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 111 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 304 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 162 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 160 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 166 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 144 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 132 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 444 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 141 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 341 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 281 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 305 KiB

View File

@ -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": {

View File

@ -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;

View File

@ -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) {

View File

@ -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",

View File

@ -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<addOnly = false> = {
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[];
};

View File

@ -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<T = {}> = Readonly<
export type RoleListType<T extends string | number | symbol = CommonRoleKeyEnum> = 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<T = {}> = Readonly<
* write: 0b110, // bad, should be 0b010
* }
*/
export type PermissionListType<T = {}> = Readonly<
export type PermissionListType<T extends string | number | symbol = CommonPerKeyEnum> = Readonly<
Record<T | CommonPerKeyEnum, PermissionValueType>
>;

View File

@ -31,11 +31,13 @@ export const TeamPerList: PermissionListType<TeamPerKeyEnum> = {
export const TeamRoleList: RoleListType<TeamRoleKeyEnum> = {
[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],

View File

@ -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 = <T extends CollaboratorItemType>({
parentClbs,
childClbs
}: {
parentClbs: T[];
childClbs: T[];
}) => {
const idToClb = new Map<string, T>();
// 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());
};

View File

@ -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 = {

View File

@ -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<boolean> =>
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);
}

View File

@ -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;

View File

@ -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
}
};

View File

@ -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);
}
};

View File

@ -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<T = any> {
code: number;

View File

@ -645,4 +645,4 @@ const createChatCompletion = async ({
}
return Promise.reject(error);
}
};
};

View File

@ -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,

View File

@ -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);

View File

@ -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);
}

View File

@ -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",

View File

@ -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);

View File

@ -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`);
};

View File

@ -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<FileTokenQuery>((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
});
});
});

View File

@ -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,

View File

@ -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<PerResourceTypeEnum, 'team'>;
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<CollaboratorItemDetailType[]> => {
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<FileTokenQuery>((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<ResourcePermissionType>[] = [];
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 });
};

View File

@ -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);

View File

@ -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,

View File

@ -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<SyncChildrenPermissionResourceType[]>()
.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<resourceId, resourceType> */
const resourceMap = new Map<string, SyncChildrenPermissionResourceType>();
/** parentChildrenMap<parentId, resourceType[]> */
const parentChildrenMap = new Map<string, SyncChildrenPermissionResourceType[]>();
// 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<resourceId, CollaboratorItemType[]>
* save the clb virtual state, not the real state at present in the DB.
*/
const resourceIdClbMap = new Map<string, ResourcePermissionType[]>();
// 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<ResourcePermissionType>[] = [];
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<ResourcePermissionType>[] = [];
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 });
}

View File

@ -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

View File

@ -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,

View File

@ -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({

View File

@ -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

View File

@ -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';

View File

@ -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 })
};
};

View File

@ -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<string, any>): Promise<TeamTmbItemTyp
}
const role =
(await getResourcePermission({
(await getTmbPermission({
resourceType: PerResourceTypeEnum.team,
teamId: tmb.teamId,
tmbId: tmb._id

View File

@ -274,7 +274,11 @@ const MySelect = <T = any,>(
w={selectItem.iconSize ?? '1rem'}
/>
)}
{selectItem?.alias || selectItem?.label || placeholder}
{
<Box noOfLines={1}>
{selectItem?.alias || selectItem?.label || placeholder}
</Box>
}
</>
)}
</>

View File

@ -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<T> = Omit<GridProps, 'onChange'> & {
defaultBg?: string;
activeBg?: string;
onChange: (e: T) => void;
isDisabled?: boolean;
};
const LeftRadio = <T = any,>({
list,
value,
align = 'flex-top',
align = 'center',
px = 3.5,
py = 4,
defaultBg = 'myGray.50',
activeBg = 'primary.50',
onChange,
isDisabled = false,
...props
}: Props<T>) => {
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 (
<Grid gridGap={[3, 5]} fontSize={['sm', 'md']} {...props}>
{list.map((item) => (
<Box
key={item.value as any}
cursor={'pointer'}
userSelect={'none'}
px={px}
py={py}
border={'base'}
borderWidth={'1px'}
borderRadius={'md'}
position={'relative'}
{...(value === item.value
? {
borderColor: list.length > 1 ? 'primary.400' : '',
bg: activeBg,
boxShadow: list.length > 1 ? 'focus' : 'none'
}
: {
bg: defaultBg,
_hover: {
borderColor: 'primary.300'
}
})}
onClick={() => onChange(item.value)}
>
{/* Circle */}
<Flex alignItems={'center'}>
{list.length > 1 && (
{list.map((item) => {
const isActive = value === item.value;
return (
<Box
key={item.value as any}
position={'relative'}
userSelect={'none'}
onClick={() => !isDisabled && onChange(item.value)}
{...getBoxStyle(isActive)}
>
<Flex alignItems={align}>
{/* Circle */}
<Box
w={'18px'}
h={'18px'}
borderWidth={'2.4px'}
borderColor={value === item.value ? 'primary.015' : 'transparent'}
borderColor={isActive ? 'primary.015' : 'transparent'}
borderRadius={'50%'}
mr={3}
>
@ -74,52 +100,59 @@ const LeftRadio = <T = any,>({
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'
})}
>
<Box
w={'5px'}
h={'5px'}
borderRadius={'50%'}
bg={value === item.value ? 'primary.600' : 'transparent'}
bg={isActive ? 'primary.600' : 'transparent'}
></Box>
</Flex>
</Box>
)}
<Box flex={'1 0 0'}>
{typeof item.title === 'string' ? (
<HStack
spacing={1}
color={'myGray.900'}
fontWeight={item.desc ? '500' : 'normal'}
whiteSpace={'nowrap'}
fontSize={'sm'}
lineHeight={1}
>
<Box>{t(item.title as any)}</Box>
{!!item.tooltip && <QuestionTip label={item.tooltip} color={'myGray.600'} />}
</HStack>
) : (
item.title
)}
<Box flex={'1 0 0'}>
{typeof item.title === 'string' ? (
<HStack
spacing={1}
fontWeight={item.desc ? 'medium' : 'normal'}
whiteSpace={'nowrap'}
fontSize={'sm'}
lineHeight={1}
color={'myGray.900'}
>
<Box>{t(item.title as any)}</Box>
{!!item.tooltip && <QuestionTip label={item.tooltip} color={'myGray.600'} />}
</HStack>
) : (
item.title
)}
{!!item.desc && (
<Box fontSize={'xs'} color={'myGray.500'} mt={1.5} lineHeight={1.2}>
{t(item.desc as any)}
</Box>
)}
</Box>
</Flex>
{item?.children && (
<Box mt={4} pt={4} borderTop={'base'} cursor={'default'}>
{item?.children}
</Box>
)}
</Box>
))}
{!!item.desc && (
<Box fontSize={'xs'} mt={1.5} lineHeight={1.2}>
{t(item.desc as any)}
</Box>
)}
</Box>
</Flex>
{item?.children && (
<Box mt={4} pt={4} borderTop={'base'} cursor={'default'}>
{item?.children}
</Box>
)}
</Box>
);
})}
</Grid>
);
};

View File

@ -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 && <VariablePickerPlugin variables={variables} />}
<OnBlurPlugin onBlur={onBlur} />
<ListDisplayFixPlugin />
<OnChangePlugin
onChange={(editorState, editor) => {
const rootElement = editor.getRootElement();
@ -232,11 +232,12 @@ export default function Editor({
{isRichText && (
<>
{/* <ListPlugin />
<ListDisplayFixPlugin />
<TabIndentationPlugin />
<ListPlugin />
<CheckListPlugin />
<ListExitPlugin /> */}
<TabToSpacesPlugin />
{/* <MarkdownPlugin /> */}
<ListExitPlugin />
<MarkdownPlugin />
</>
)}
</>

View File

@ -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

View File

@ -23,3 +23,88 @@ export type EditorVariableLabelPickerType = {
};
export type FormPropsType = Omit<BoxProps, 'onChange' | 'onBlur'>;
// 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<ChildEditorNode | ListEditorNode>;
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<ParagraphEditorNode | ListEditorNode>;
direction: string;
format: string;
indent: number;
} & BaseEditorNode;
};
export type ListItemInfo = {
type: 'bullet' | 'number';
text: string;
indent: number;
numberValue?: number;
};

View File

@ -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<T extends TextNode | VariableLabelNode | VariableNode>(
editor: LexicalEditor,
@ -175,31 +182,148 @@ export function registerLexicalTextEntity<T extends TextNode | VariableLabelNode
return [removePlainTextTransform, removeReverseNodeTransform];
}
export function textToEditorState(text = '') {
const paragraph = typeof text === 'string' ? text?.split('\n') : [''];
// text to editor state
const parseTextLine = (line: string) => {
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<ParagraphEditorNode | ListEditorNode> = [];
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<string, boolean> = {};
// 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');
}
};

View File

@ -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<DataT, ResT = {}>(
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<DataT, ResT = {}>(
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<DataT, ResT = {}>(
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<HTMLDivElement>) => {
@ -92,6 +97,16 @@ export function usePagination<DataT, ResT = {}>(
});
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<DataT, ResT = {}>(
// 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<DataT, ResT = {}>(
);
// 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<DataT, ResT = {}>(
throttleWait: 100
}
);
// Page size refresh
useEffect(() => {
data.length > 0 && fetchData();
}, [pageSize]);

View File

@ -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",

View File

@ -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",

View File

@ -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",

View File

@ -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",

View File

@ -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",

View File

@ -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": "保留管理员权限",

View File

@ -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": "可查看和编辑应用",

View File

@ -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": "写权限",

View File

@ -133,7 +133,7 @@
"llm_paragraph_mode_force_desc": "强制使用模型自动识别段落,并忽略原文本的段落(如有)",
"loading": "加载中...",
"max_chunk_size": "最大分块大小",
"move.hint": "移动后,所选知识库/文件夹将继承新文件夹的权限设置,原先的权限设置失效。",
"move.hint": "移动后,所选知识库/文件夹将继承新文件夹的权限设置。",
"noChildren": "无子目录",
"noSelectedFolder": "没有选择文件夹",
"noSelectedId": "没有选择 ID",

View File

@ -21,7 +21,6 @@
"delete.admin_success": "删除管理员成功",
"delete.failed": "删除失败",
"delete.success": "删除成功",
"has_chosen": "已选择",
"login.Dingtalk": "钉钉登录",
"login.error": "登录异常",
"login.password_condition": "密码最多 60 位",

View File

@ -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": "保留管理員權限",

View File

@ -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": "可以檢視和編輯應用程式",

View File

@ -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": "寫入權限",

View File

@ -133,7 +133,7 @@
"llm_paragraph_mode_force_desc": "強制使用模型自動識別段落,並忽略原文本的段落(如有)",
"loading": "加載中...",
"max_chunk_size": "最大分塊大小",
"move.hint": "移動後,所選資料集/資料夾將繼承新資料夾的權限設定,原先的權限設定將失效。",
"move.hint": "移動後,所選知識庫/文件夾將繼承新文件夾的權限設置。",
"noChildren": "無子目錄",
"noSelectedFolder": "沒有選擇文件夾",
"noSelectedId": "沒有選擇 ID",

View File

@ -21,7 +21,6 @@
"delete.admin_success": "刪除管理員成功",
"delete.failed": "刪除失敗",
"delete.success": "刪除成功",
"has_chosen": "已選擇",
"login.Dingtalk": "釘釘登入",
"login.error": "登入失敗",
"login.password_condition": "密碼最多可輸入 60 個字元",

View File

@ -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",

View File

@ -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",

View File

@ -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",

View File

@ -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

View File

@ -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",

View File

@ -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]);
};

View File

@ -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,

View File

@ -137,7 +137,7 @@ const FolderSlideCard = ({
isInheritPermission={isInheritPermission}
hasParent={hasParent}
>
{({ MemberListCard, onOpenManageModal, onOpenAddMember }) => {
{({ MemberListCard, onOpenManageModal }) => {
return (
<>
<Flex alignItems="center" justifyContent="space-between">
@ -145,26 +145,15 @@ const FolderSlideCard = ({
{t('common:permission.Collaborator')}
</Box>
{managePer.permission.hasManagePer && (
<HStack spacing={3}>
<MyTooltip label={t('common:permission.Manage')}>
<MyIcon
w="1rem"
name="common/settingLight"
cursor={'pointer'}
_hover={{ color: 'primary.600' }}
onClick={onOpenManageModal}
/>
</MyTooltip>
<MyTooltip label={t('common:Add')}>
<MyIcon
w="1rem"
name="support/permission/collaborator"
cursor={'pointer'}
_hover={{ color: 'primary.600' }}
onClick={onOpenAddMember}
/>
</MyTooltip>
</HStack>
<MyTooltip label={t('common:permission.Manage')}>
<MyIcon
w="1rem"
name="common/settingLight"
cursor={'pointer'}
_hover={{ color: 'primary.600' }}
onClick={onOpenManageModal}
/>
</MyTooltip>
)}
</Flex>
<MemberListCard

View File

@ -124,19 +124,21 @@ const VariableInput = ({
bg={'white'}
boxShadow={'0 0 8px rgba(0,0,0,0.15)'}
>
<Flex
color={'primary.600'}
bg={'primary.100'}
mb={3}
px={3}
py={1.5}
gap={1}
fontSize={'mini'}
rounded={'sm'}
>
<MyIcon name={'common/info'} color={'primary.600'} w={4} />
{t('chat:variable_invisable_in_share')}
</Flex>
{chatType !== ChatTypeEnum.chat && (
<Flex
color={'primary.600'}
bg={'primary.100'}
mb={3}
px={3}
py={1.5}
gap={1}
fontSize={'mini'}
rounded={'sm'}
>
<MyIcon name={'common/info'} color={'primary.600'} w={4} />
{t('chat:variable_invisable_in_share')}
</Flex>
)}
{externalVariableList.map((item) => {
return (
<LabelAndFormRender

View File

@ -33,19 +33,6 @@ const ChatHomeVariablesForm = ({ chatForm }: Props) => {
{/* custom variables */}
{allVariableList.filter((i) => i.type === VariableInputEnum.custom).length > 0 && (
<>
<Flex
color={'primary.600'}
bg={'primary.100'}
mb={3}
px={3}
py={1.5}
gap={1}
fontSize={'mini'}
rounded={'sm'}
>
<MyIcon name={'common/info'} color={'primary.600'} w={4} />
{t('chat:variable_invisable_in_share')}
</Flex>
{allVariableList
.filter((i) => i.type === VariableInputEnum.custom)
.map((item) => (

View File

@ -1126,7 +1126,8 @@ const ChatBox = ({
>
<Flex h={'100%'} flexDir={'column'} justifyContent={'center'} w={'100%'}>
{HomeChatRenderBox}
{allVariableList.length > 0 ? (
{allVariableList.filter((item) => item.type !== VariableInputEnum.internal).length >
0 ? (
<Box w={'100%'}>
<ChatHomeVariablesForm chatForm={chatForm} />
</Box>

View File

@ -118,19 +118,21 @@ const VariablePopover = ({ chatType }: { chatType: ChatTypeEnum }) => {
{externalVariableList.length > 0 && (
<Box textAlign={'left'}>
<Flex
color={'primary.600'}
bg={'primary.100'}
mb={3}
px={3}
py={1.5}
gap={1}
fontSize={'mini'}
rounded={'sm'}
>
<MyIcon name={'common/info'} color={'primary.600'} w={4} />
{t('chat:variable_invisable_in_share')}
</Flex>
{chatType !== ChatTypeEnum.chat && (
<Flex
color={'primary.600'}
bg={'primary.100'}
mb={3}
px={3}
py={1.5}
gap={1}
fontSize={'mini'}
rounded={'sm'}
>
<MyIcon name={'common/info'} color={'primary.600'} w={4} />
{t('chat:variable_invisable_in_share')}
</Flex>
)}
{externalVariableList.map((item) => (
<LabelAndFormRender
{...item}

View File

@ -12,6 +12,7 @@ import {
import InputRender from '@/components/core/app/formRender';
import { nodeInputTypeToInputType } from '@/components/core/app/formRender/utils';
import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel';
import LeftRadio from '@fastgpt/web/components/common/Radio/LeftRadio';
const DescriptionBox = React.memo(function DescriptionBox({
description
@ -38,33 +39,26 @@ export const SelectOptionsComponent = React.memo(function SelectOptionsComponent
return (
<Box maxW={'100%'}>
<DescriptionBox description={description} />
<Flex flexDirection={'column'} gap={3} w={'250px'}>
{userSelectOptions.map((option: UserSelectOptionItemType) => {
const selected = option.value === userSelectedVal;
return (
<Button
key={option.key}
variant={'whitePrimary'}
whiteSpace={'pre-wrap'}
isDisabled={!!userSelectedVal}
{...(selected
? {
_disabled: {
cursor: 'default',
borderColor: 'primary.300',
bg: 'primary.50 !important',
color: 'primary.600'
}
}
: {})}
onClick={() => onSelect(option.value)}
>
{option.value}
</Button>
);
})}
</Flex>
<Box w={'250px'}>
<LeftRadio<string>
py={3.5}
gridGap={3}
align={'center'}
list={userSelectOptions.map((option: UserSelectOptionItemType) => ({
title: (
<Box fontSize={'sm'} whiteSpace={'pre-wrap'} wordBreak={'break-word'}>
{option.value}
</Box>
),
value: option.value
}))}
value={userSelectedVal || ''}
defaultBg={'white'}
activeBg={'white'}
onChange={(val) => onSelect(val)}
isDisabled={!!userSelectedVal}
/>
</Box>
</Box>
);
});

View File

@ -67,7 +67,7 @@ const ConfigPerModal = ({
isInheritPermission={isInheritPermission}
hasParent={hasParent}
>
{({ MemberListCard, onOpenManageModal, onOpenAddMember }) => {
{({ MemberListCard, onOpenManageModal }) => {
return (
<>
<Flex
@ -77,24 +77,14 @@ const ConfigPerModal = ({
w="full"
>
<Box fontSize={'sm'}>{t('common:permission.Collaborator')}</Box>
<Flex flexDirection="row" gap="2">
<Button
size="sm"
variant="whitePrimary"
leftIcon={<MyIcon w="4" name="common/settingLight" />}
onClick={onOpenManageModal}
>
{t('common:permission.Manage')}
</Button>
<Button
size="sm"
variant="whitePrimary"
leftIcon={<MyIcon w="4" name="support/permission/collaborator" />}
onClick={onOpenAddMember}
>
{t('common:Add')}
</Button>
</Flex>
<Button
size="sm"
variant="whitePrimary"
leftIcon={<MyIcon w="4" name="common/settingLight" />}
onClick={onOpenManageModal}
>
{t('common:permission.Manage')}
</Button>
</Flex>
<MemberListCard mt={2} p={1.5} bg="myGray.100" borderRadius="md" />
</>

View File

@ -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 (
<MyModal
isOpen
onClose={onClose}
minW="600px"
title={t('user:team.manage_collaborators')}
iconSrc="common/settingLight"
>
<ModalBody>
<TableContainer borderRadius="md" minH="400px">
<Table>
<Thead bg="myGray.100">
<Tr>
<Th border="none">{t('user:name')}</Th>
<Th border="none">{t('user:permissions')}</Th>
<Th border="none" w={'40px'}>
{t('user:operations')}
</Th>
</Tr>
</Thead>
<Tbody>
<Tr h={'10px'} />
{collaboratorList?.map((item) => {
return (
<Tr
key={item.tmbId}
_hover={{
bg: 'myGray.50'
}}
>
<Td border="none">
<Flex alignItems="center">
<Avatar src={item.avatar} rounded={'50%'} w="24px" mr={2} />
{item.name === DefaultGroupName ? userInfo?.team.teamName : item.name}
</Flex>
</Td>
<Td border="none">
<RoleTags permission={item.permission.role} />
</Td>
<Td border="none">
{/* Not self; Not owner and other manager */}
{item.tmbId !== userInfo?.team?.tmbId &&
(permission.isOwner || !item.permission.hasManagePer) && (
<RoleSelect
Button={
<MyIcon name={'edit'} w={'16px'} _hover={{ color: 'primary.600' }} />
}
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;
}>);
}}
/>
)}
</Td>
</Tr>
);
})}
</Tbody>
</Table>
{collaboratorList?.length === 0 && <EmptyTip text={t('user:team.no_collaborators')} />}
</TableContainer>
{loading && <Loading fixed={false} />}
</ModalBody>
</MyModal>
);
}
export default ManageModal;

View File

@ -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 (
<HStack
<Flex
justifyContent="space-between"
alignItems="center"
key={key}
px="3"
py="2"
px="1"
py="1"
gap="2"
borderRadius="sm"
_hover={{
bgColor: 'myGray.50',
cursor: 'pointer'
{...(!showRoleSelect
? {
_hover: { bgColor: 'myGray.50' },
cursor: 'pointer'
}
: {})}
onClick={() => {
if (disabled) return;
onChange?.();
}}
onClick={onChange}
>
{isChecked !== undefined && (
<Checkbox isChecked={isChecked} pointerEvents="none" disabled={isAdded} />
)}
<Avatar src={avatar} w="1.5rem" borderRadius={'50%'} />
<Box w="full">
<Box fontSize={'sm'} className="textEllipsis" maxW="300px">
{name}
<Flex
flexDirection={'row'}
h={showRoleSelect ? '36px' : 'unset'}
p="1"
alignItems={'center'}
gap="2"
w="full"
>
{isChecked !== undefined && (
<Checkbox isDisabled={disabled} isChecked={isChecked} pointerEvents="none" />
)}
<Avatar src={avatar} w="1.5rem" borderRadius={'50%'} />
<Box flex={'1 0 0'} w={0}>
<Box fontSize={'sm'} w={'100%'} noOfLines={1}>
{name === DefaultGroupName ? userInfo?.team.teamName : name}
</Box>
<Box lineHeight={1} w={'100%'}>
{orgs && orgs.length > 0 && <OrgTags orgs={orgs} />}
</Box>
</Box>
<Box lineHeight={1}>{orgs && orgs.length > 0 && <OrgTags orgs={orgs} />}</Box>
</Box>
{!isAdded && role && <RoleTags permission={role} />}
{isAdded && (
<Tag
mixBlendMode={'multiply'}
colorSchema="blue"
border="none"
py={2}
px={3}
fontSize={'xs'}
>
{t('user:team.collaborator.added')}
</Tag>
)}
{onDelete !== undefined && (
<MyIcon
name="common/closeLight"
w="1rem"
cursor={'pointer'}
_hover={{
color: 'red.600'
}}
onClick={onDelete}
</Flex>
{showRoleSelect && (
<RoleSelect
disabled={disabled}
value={role}
Button={
<Flex
bg={'myGray.50'}
border="base"
fontSize={'sm'}
borderRadius={'md'}
minH={'18px'}
w="300px"
p="1"
alignItems={'end'}
justifyContent={'space-between'}
>
<RoleTags permission={role} />
<Flex h="18px" alignItems={'center'} justifyContent={'center'}>
<ChevronDownIcon fontSize="md" />
</Flex>
</Flex>
}
onChange={onRoleChange}
/>
)}
{rightSlot}
</HStack>
<Flex flexDirection={'row'} h={showRoleSelect ? '36px' : 'unset'} alignItems={'center'}>
{onDelete !== undefined && !disabled ? (
<MyIcon
name="common/closeLight"
w="1rem"
cursor={disabled ? 'not-allowed' : 'pointer'}
_hover={{
color: 'red.600'
}}
onClick={() => {
if (disabled) return;
onDelete?.();
}}
/>
) : (
<Box minW="16px"></Box>
)}
</Flex>
{!!rightSlot && rightSlot}
</Flex>
);
}

View File

@ -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<false>({
@ -91,34 +87,33 @@ function MemberModal({
}
);
const [selectedOrgList, setSelectedOrgIdList] = useState<OrgListItemType[]>([]);
const [editCollaborators, setCollaboratorList] = useState<CollaboratorItemDetailType[]>([]);
const [selectedMemberList, setSelectedMemberList] = useState<
Omit<TeamMemberItemType, 'permission' | 'teamId'>[]
>([]);
const [selectedGroupList, setSelectedGroupList] = useState<MemberGroupListItemType<false>[]>([]);
const roleList = useContextSelector(CollaboratorContext, (v) => v.roleList);
const getRoleLabelList = useContextSelector(CollaboratorContext, (v) => v.getRoleLabelList);
const [selectedRole, setSelectedRole] = useState<number | undefined>(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<ValueOf<typeof addOnly>>),
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 (
<MyModal
isOpen
onClose={onClose}
iconSrc={addOnly ? 'keyPrimary' : 'modal/AddClb'}
title={addOnly ? t('user:team.add_permission') : t('user:team.add_collaborator')}
minW="800px"
maxW={'60vw'}
h={'100%'}
maxH={'90vh'}
isCentered
isLoading={loadingGroupsAndOrgs}
>
<ModalBody flex={'1'}>
<Grid
border="1px solid"
borderColor="myGray.200"
borderRadius="0.5rem"
gridTemplateColumns="1fr 1fr"
h={'100%'}
>
<Flex
h={'100%'}
flexDirection="column"
borderRight="1px solid"
<>
<MyModal
isOpen
onClose={onClose}
iconSrc={'common/settingLight'}
title={t('user:team.manage_collaborators')}
minW="900px"
maxW={'60vw'}
h={'100%'}
isCentered
isLoading={loadingGroupsAndOrgs}
>
<ModalBody flex={'1'}>
<Grid
border="1px solid"
borderColor="myGray.200"
p="4"
borderRadius="0.5rem"
gridTemplateColumns="40% 60%"
h={'100%'}
>
<SearchInput
placeholder={t('user:search_group_org_user')}
bgColor="myGray.50"
onChange={(e) => setSearchKey(e.target.value)}
/>
<Flex
h={'100%'}
flexDirection="column"
borderRight="1px solid"
borderColor="myGray.200"
py="2"
>
<Box px={2}>
<SearchInput
placeholder={t('user:search_group_org_user')}
bgColor="myGray.50"
onChange={(e) => setSearchKey(e.target.value)}
/>
</Box>
<Flex flexDirection="column" mt="3" overflow={'auto'} flex={'1 0 0'} h={0}>
{/* Entry */}
{!searchKey && !filterClass && (
<>
{entryList.current.map((item) => {
return (
<HStack
key={item.value}
justifyContent="space-between"
py="2"
px="3"
borderRadius="sm"
alignItems="center"
_hover={HoverBoxStyle}
_notLast={{ mb: 1 }}
onClick={() => setFilterClass(item.value as any)}
<Flex flexDirection="column" mt="3" overflow={'auto'} flex={'1 0 0'} h={0}>
{/* Entry */}
{!searchKey && !filterClass && (
<Box px={2}>
{entryList.current.map((item) => {
return (
<HStack
key={item.value}
justifyContent="space-between"
py="2"
px="3"
borderRadius="sm"
alignItems="center"
_hover={HoverBoxStyle}
_notLast={{ mb: 1 }}
onClick={() => setFilterClass(item.value as any)}
>
<MyAvatar src={item.icon} w="1.5rem" borderRadius={'50%'} />
<Box ml="2" w="full">
{item.label}
</Box>
<MyIcon name="core/chat/chevronRight" w="16px" />
</HStack>
);
})}
</Box>
)}
{/* Path */}
{!searchKey && filterClass && (
<Box mb={1} px={2}>
<Path
paths={[
{
parentId: filterClass,
parentName:
filterClass === 'member'
? t('user:team.group.members')
: filterClass === 'org'
? t('user:team.org.org')
: t('user:team.group.group')
},
...paths
]}
onClick={(parentId) => {
if (parentId === '') {
setFilterClass(undefined);
onPathClick('');
} else if (
parentId === 'member' ||
parentId === 'org' ||
parentId === 'group'
) {
setFilterClass(parentId);
onPathClick('');
} else {
onPathClick(parentId);
}
}}
rootName={t('common:Team')}
/>
</Box>
)}
{(filterClass === 'member' || searchKey) &&
(() => {
const MemberList = (
<RenderMemberList
members={memberWithPer}
setCollaboratorList={setCollaboratorList}
editCollaborators={editCollaborators}
defaultRole={defaultRole}
/>
);
return searchKey ? (
<Box px={2}>{MemberList}</Box>
) : (
<TeamMemberScrollData
flexDirection={'column'}
gap={1}
userSelect={'none'}
height={'fit-content'}
px={2}
>
<MyAvatar src={item.icon} w="1.5rem" borderRadius={'50%'} />
<Box ml="2" w="full">
{item.label}
</Box>
<MyIcon name="core/chat/chevronRight" w="16px" />
</HStack>
{MemberList}
</TeamMemberScrollData>
);
})}
</>
)}
})()}
{(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 (
<MemberItemCard
avatar={org.avatar}
key={org._id}
name={org.name}
onChange={addTheOrg}
isChecked={isChecked}
rightSlot={
org.total && (
<MyIcon
name="core/chat/chevronRight"
w="16px"
p="4px"
rounded={'6px'}
_hover={{
bgColor: 'myGray.200'
}}
onClick={(e) => {
onClickOrg(org);
e.stopPropagation();
}}
/>
)
}
/>
);
});
return searchKey ? (
<Box px={2}>{Orgs}</Box>
) : (
<OrgMemberScrollData px={2}>
{Orgs}
<RenderMemberList
members={orgMembersWithPer}
setCollaboratorList={setCollaboratorList}
editCollaborators={editCollaborators}
defaultRole={defaultRole}
/>
</OrgMemberScrollData>
);
})()}
{(filterClass === 'group' || searchKey) && (
<Box
{...(searchKey
? {}
: {
flex: '1 0 0',
overflow: 'auto'
})}
px={2}
>
{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 (
<MemberItemCard
avatar={group.avatar}
key={group._id}
name={
group.name === DefaultGroupName
? userInfo?.team.teamName ?? ''
: group.name
}
onChange={addGroup}
isChecked={isChecked}
/>
);
})}
</Box>
)}
</Flex>
</Flex>
{/* Path */}
{!searchKey && filterClass && (
<Box mb={1}>
<Path
paths={[
{
parentId: filterClass,
parentName:
filterClass === 'member'
? t('user:team.group.members')
: filterClass === 'org'
? t('user:team.org.org')
: t('user:team.group.group')
},
...paths
]}
onClick={(parentId) => {
if (parentId === '') {
setFilterClass(undefined);
onPathClick('');
} else if (
parentId === 'member' ||
parentId === 'org' ||
parentId === 'group'
) {
setFilterClass(parentId);
onPathClick('');
} else {
onPathClick(parentId);
}
}}
rootName={t('common:Team')}
/>
</Box>
)}
{(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 (
<MemberItemCard
addOnly={addOnly}
avatar={member.avatar}
key={member.tmbId}
name={member.memberName}
role={collaborator?.permission.role}
onChange={onChange}
isChecked={!!selectedMemberList.find((v) => v.tmbId === member.tmbId)}
orgs={member.orgs}
/>
);
});
return searchKey ? (
Members
) : (
<TeamMemberScrollData
flexDirection={'column'}
gap={1}
userSelect={'none'}
height={'fit-content'}
>
{Members}
</TeamMemberScrollData>
);
})()}
{(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 (
<MemberItemCard
avatar={org.avatar}
key={org._id}
name={org.name}
onChange={onChange}
addOnly={addOnly}
role={collaborator?.permission.role}
isChecked={!!selectedOrgList.find((v) => String(v._id) === String(org._id))}
rightSlot={
org.total && (
<MyIcon
name="core/chat/chevronRight"
w="16px"
p="4px"
rounded={'6px'}
_hover={{
bgColor: 'myGray.200'
}}
onClick={(e) => {
onClickOrg(org);
// setPath(getOrgChildrenPath(org));
e.stopPropagation();
}}
/>
)
}
/>
);
});
return searchKey ? (
Orgs
) : (
<OrgMemberScrollData>
{Orgs}
{orgMembers.map((member) => {
const isChecked = !!selectedMemberList.find(
(v) => v.tmbId === member.tmbId
);
const collaborator = collaboratorList?.find(
(v) => v.tmbId === member.tmbId
);
return (
<MemberItemCard
avatar={member.avatar}
key={member.tmbId}
name={member.memberName}
onChange={() => {
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}
/>
);
})}
</OrgMemberScrollData>
);
})()}
{(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];
<Flex h={'100%'} flexDirection="column" overflow={'auto'} p="2">
<Box mt={2} mb={3}>{`${t('common:chosen')}: ${editCollaborators.length}`}</Box>
<Flex flexDirection="column" gap={1} flex={'1 0 0'} h={0}>
{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 (
<MemberItemCard
avatar={group.avatar}
key={group._id}
name={
group.name === DefaultGroupName ? userInfo?.team.teamName ?? '' : group.name
key={'chosen-' + getCollaboratorId(clb)}
avatar={clb.avatar}
name={clb.name ?? ''}
onDelete={onDelete}
role={clb.permission.role}
onRoleChange={onRoleChange}
disabled={
clb.permission.role === OwnerRoleVal ||
clb.tmbId === userInfo?.team.tmbId ||
(clb.permission.hasManagePer && !myRole.isOwner)
}
role={collaborator?.permission.role}
onChange={onChange}
isChecked={!!selectedGroupList.find((v) => v._id === group._id)}
addOnly={addOnly}
/>
);
})}
</Flex>
</Flex>
<Flex h={'100%'} p="4" flexDirection="column">
<Box>
{`${t('user:has_chosen')}: `}
{selectedMemberList.length + selectedGroupList.length + selectedOrgList.length}
</Box>
<Flex flexDirection="column" mt="2" gap={1} overflow={'auto'} flex={'1 0 0'} h={0}>
{selectedList.map((item) => {
return (
<MemberItemCard
key={item.id}
avatar={item.avatar}
name={item.name ?? ''}
onChange={item.onDelete}
onDelete={item.onDelete}
orgs={item?.orgs}
/>
);
})}
</Flex>
</Flex>
</Grid>
</ModalBody>
<ModalFooter>
{!addOnly && !!roleList && (
<RoleSelect
value={selectedRole}
Button={
<Flex
alignItems={'center'}
bg={'myGray.50'}
border="base"
fontSize={'sm'}
px={3}
borderRadius={'md'}
h={'32px'}
>
{roleLabel}
<ChevronDownIcon fontSize={'md'} />
</Flex>
}
onChange={(v) => setSelectedRole(v)}
/>
)}
{addOnly && (
<HStack bg={'blue.50'} color={'blue.600'} padding={'6px 12px'} rounded={'5px'}>
<MyIcon name="common/info" w="1rem" h="1rem" />
<Text fontSize="12px">{t('user:permission_add_tip')}</Text>
</HStack>
)}
<Button isLoading={isUpdating} ml="4" h={'32px'} onClick={onConfirm}>
{t('common:Confirm')}
</Button>
</ModalFooter>
</MyModal>
</Flex>
</Grid>
</ModalBody>
<ModalFooter>
<Button isLoading={isUpdating} ml="4" h={'32px'} onClick={onConfirm}>
{t('common:Confirm')}
</Button>
</ModalFooter>
</MyModal>
<ConfirmDisableInheritPer />
</>
);
}
export default MemberModal;
const RenderMemberList = ({
members,
setCollaboratorList,
editCollaborators,
defaultRole
}: {
members: Array<Omit<TeamMemberItemType, 'permission'> & { permission: Permission }>;
setCollaboratorList: React.Dispatch<React.SetStateAction<CollaboratorItemDetailType[]>>;
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 (
<MemberItemCard
role={member.permission.role}
avatar={member.avatar}
key={member.tmbId}
name={member.memberName}
onChange={addTheMember}
isChecked={isChecked}
orgs={member.orgs}
disabled={
member.permission.role === OwnerRoleVal ||
member.tmbId === userInfo?.team.tmbId ||
(member.permission.hasManagePer && !myRole.isOwner)
}
/>
);
})}
</>
);
};

View File

@ -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<HTMLDivElement>(null);
const closeTimer = useRef<NodeJS.Timeout>();
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({
>
<MenuButton
position={'relative'}
cursor={disabled ? 'not-allowed' : 'pointer'}
onClickCapture={() => {
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 && (
<>
<MyDivider />
<Box pb="2" px="3" fontSize={'sm'} color={'myGray.900'}>
@ -197,7 +203,8 @@ function RoleSelect({
)}
{roleOptions.checkboxList.map((item) => {
const change = () => {
const change = (e: React.MouseEvent<HTMLDivElement, 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);
}}
>
<Checkbox size="sm" isChecked={isChecked} onChange={change} />
<Flex ml={4} flexDirection="column" flex={'1 0 0'} onClick={change}>
<Checkbox size="sm" isChecked={isChecked} />
<Flex ml={4} flexDirection="column" flex={'1 0 0'}>
<Box>{t(item.name as any)}</Box>
<Box color={'myGray.500'} fontSize={'mini'}>
{t(item.description as any)}

View File

@ -19,16 +19,16 @@ function RoleTags({ permission }: PermissionTagsProp) {
const roleTagList = getRoleLabelList(permission);
return (
<Flex gap="2" alignItems="center">
<Flex gap="2" alignItems="center" flexWrap="wrap" minH="15px">
{roleTagList.map((item) => (
<Tag
mixBlendMode={'multiply'}
key={item}
colorSchema="blue"
border="none"
py={2}
px={3}
fontSize={'xs'}
py={1}
px={2}
fontSize={'2xs'}
>
{t(item as any)}
</Tag>

Some files were not shown because too many files have changed in this diff Show More