Compare commits

...

12 Commits

Author SHA1 Message Date
heheer
2d82db01a9
fix: api dataset reference tag preview (#3600) 2025-01-15 14:47:57 +08:00
Archer
80e670600b
perf: load members;perf: yuque load;fix: workflow llm params cannot close (#3594)
* chat openapi doc

* feat: dataset openapi doc

* perf: load members

* perf: member load code

* perf: yuque load

* fix: workflow llm params cannot close
2025-01-15 14:47:56 +08:00
heheer
68f5afeba0
fix: yuque dataset file folder can enter (#3593) 2025-01-15 14:47:56 +08:00
Finley Ge
0d9f54cbf3
fix: scroll fetch (#3592) 2025-01-15 14:47:55 +08:00
Finley Ge
11cfe8a809
fix: collection list api old version (#3591)
* fix: collection list api format

* fix: type error of addSourceMemeber
2025-01-15 14:47:55 +08:00
Archer
6f8c6b6ad1
4.8.19 test (#3584)
* faet: dataset search filter

* fix: scroll page
2025-01-15 14:47:54 +08:00
Finley Ge
f468ba2f30
fix: pagination bug (#3577) 2025-01-15 14:47:54 +08:00
Finley Ge
19abfd1a3e
docs: add custom uid docs (#3572) 2025-01-15 14:47:54 +08:00
Jiangween
ace304c619
docs:更新用户答疑 (#3576) 2025-01-15 14:47:54 +08:00
archer
923d0f85e9
perf: list data 2025-01-15 14:47:53 +08:00
archer
62bcff2ff0
perf: scroll page code 2025-01-15 14:47:53 +08:00
Finley Ge
ec0cef09a2
feat: sync org from wecom, pref: member list pagination (#3549)
* feat: sync org

* chore: fe

* chore: loading

* chore: type

* pref: team member list change to pagination. Edit a sort of list apis.

* feat: member update avatar

* chore: user avatar move to tmb

* chore: init scripts move user avatar

* chore: sourceMember

* fix: list api sourceMember

* fix: member sync

* fix: pagination

* chore: adjust code

* chore: move changeOwner to pro

* chore: init v4819 script

* chore: adjust code

* chore: UserBox
2025-01-15 14:47:50 +08:00
131 changed files with 1978 additions and 1334 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

View File

@ -19,6 +19,16 @@ images: []
## 二、通用问题 ## 二、通用问题
### 通过sealos部署的话是否没有本地部署的一些限制
![](/imgs/faq1.png)
这是索引模型的长度限制,通过任何方式部署都一样的,但不同索引模型的配置不一样,可以在后台修改参数。
### sealos怎么挂载 小程序配置文件
新增配置文件:/app/projects/app/public/xxxx.txt
如图
![](/imgs/faq2.png)
### 本地部署的限制 ### 本地部署的限制
具体内容参考https://fael3z0zfze.feishu.cn/wiki/OFpAw8XzAi36Guk8dfucrCKUnjg。 具体内容参考https://fael3z0zfze.feishu.cn/wiki/OFpAw8XzAi36Guk8dfucrCKUnjg。

View File

@ -56,3 +56,27 @@ curl --location --request POST 'https://api.fastgpt.in/api/v1/chat/completions'
] ]
}' }'
``` ```
## 自定义用户 ID
`v4.8.13`后支持传入自定义的用户 ID, 并且存入历史记录中。
```sh
curl --location --request POST 'https://api.fastgpt.in/api/v1/chat/completions' \
--header 'Authorization: Bearer fastgpt-xxxxxx' \
--header 'Content-Type: application/json' \
--data-raw '{
"chatId": "111",
"stream": false,
"detail": false,
"messages": [
{
"content": "导演是谁",
"role": "user"
}
],
"customUid": "xxxxxx"
}'
```
在历史记录中,该条记录的使用者会显示为 `xxxxxx`

View File

@ -686,7 +686,7 @@ curl --location --request POST 'http://localhost:3000/api/core/chat/getHistories
- appId - 应用 Id - appId - 应用 Id
- offset - 偏移量,即从第几条数据开始取 - offset - 偏移量,即从第几条数据开始取
- pageSize - 记录数量 - pageSize - 记录数量
- source - 对话源 - source - 对话源。source=api表示获取通过 API 创建的对话(不会获取到页面上的对话记录)
{{% /alert %}} {{% /alert %}}
{{< /markdownify >}} {{< /markdownify >}}

View File

@ -733,6 +733,21 @@ data 为集合的 ID。
{{< tab tabName="请求示例" >}} {{< tab tabName="请求示例" >}}
{{< markdownify >}} {{< markdownify >}}
**4.8.19+**
```bash
curl --location --request POST 'http://localhost:3000/api/core/dataset/collection/listv2' \
--header 'Authorization: Bearer {{authorization}}' \
--header 'Content-Type: application/json' \
--data-raw '{
"offset":0,
"pageSize": 10,
"datasetId":"6593e137231a2be9c5603ba7",
"parentId": null,
"searchText":""
}'
```
**4.8.19-(不再维护)**
```bash ```bash
curl --location --request POST 'http://localhost:3000/api/core/dataset/collection/list' \ curl --location --request POST 'http://localhost:3000/api/core/dataset/collection/list' \
--header 'Authorization: Bearer {{authorization}}' \ --header 'Authorization: Bearer {{authorization}}' \
@ -753,7 +768,7 @@ curl --location --request POST 'http://localhost:3000/api/core/dataset/collectio
{{< markdownify >}} {{< markdownify >}}
{{% alert icon=" " context="success" %}} {{% alert icon=" " context="success" %}}
- pageNum: 页码(选填) - offset: 偏移量
- pageSize: 每页数量最大30选填 - pageSize: 每页数量最大30选填
- datasetId: 知识库的ID(必填) - datasetId: 知识库的ID(必填)
- parentId: 父级Id选填 - parentId: 父级Id选填
@ -773,9 +788,7 @@ curl --location --request POST 'http://localhost:3000/api/core/dataset/collectio
"statusText": "", "statusText": "",
"message": "", "message": "",
"data": { "data": {
"pageNum": 1, "list": [
"pageSize": 10,
"data": [
{ {
"_id": "6593e137231a2be9c5603ba9", "_id": "6593e137231a2be9c5603ba9",
"parentId": null, "parentId": null,

View File

@ -0,0 +1,17 @@
---
title: 'V4.8.19(进行中)'
description: 'FastGPT V4.8.19 更新说明'
icon: 'upgrade'
draft: false
toc: true
weight: 806
---
## 完整更新内容
1. 新增 - 工作流知识库检索支持按知识库权限进行过滤。
2. 优化 - 成员列表分页加载。
3. 优化 - 统一分页加载代码。
4. 修复 - 语雀文件库导入时,嵌套文件内容无法展开的问题。
5. 修复 - 工作流编排中LLM 参数无法关闭问题。

View File

@ -38,3 +38,32 @@ A: 通常是由于上下文不一致导致,可以在对话日志中,找到
| | | | | | | |
| --- | --- | --- | | --- | --- | --- |
| ![](/imgs/image-85.png) | ![](/imgs/image-86.png) | ![](/imgs/image-87.png) | | ![](/imgs/image-85.png) | ![](/imgs/image-86.png) | ![](/imgs/image-87.png) |
在针对知识库的回答要求里有, 要给它配置提示词,不然他就是默认的,默认的里面就有该语法。
## 工作流操作一个工作流以一个问题分类节点开始根据不同的分类导入到不同的分支访问相应的知识库和AI对话AI对话返回内容后怎么样不进入问题分类节点而是将问题到知识库搜索然后把历史记录一起作为背景再次AI查询。
做个判断器如果是初次开始对话也就是历史记录为0就走问题分类不为零直接走知识库和ai。
## 实时对话,设置 fastgpt 定时,比如每隔 3000MS 去拿一次 webhook发送过来的消息到AI页面
定时执行没有这么高频率的去拿信息的,想要实现在企微里面的实时对话的机器人,
目前通过低代码的工作流构建应该是不行的,只能自己写代码,然后去调用 FastGPT 的 APIKey 回复。企业微信似乎没有提供「自动监听」群聊消息的接口(或是通过 at 机器人这种触发消息推送)。应该只能发消息给应用,接收这个 https://developer.work.weixin.qq.com/document/path/90238 文档中的消息推送实现实时对话。或者是定时去拿群聊消息通过这个文档所示的接口https://developer.work.weixin.qq.com/document/path/98914然后用这个接口 https://developer.work.weixin.qq.com/document/path/90248 去推送消息。
## 工作流连接数据库
工作流提供该连接数据库功能,用这个数据库连接的 plugin 可以实现 text2SQL但是相对危险不建议做写入等操作。
![](/imgs/quizApp1.png)
## 关于循环体,协助理解循环体的循环条件和终止条件、循环的方式,循环体内参数调用后、在循环体内属于是局部作用域的参数还是全局作用域的参数
可理解为 for 函数,传一个数组,每个数据都执行一次。
## 公式无法正常显示
添加相关提示词,引导模型按 Markdown 输出公式
```bash
Latex inline: \(x^2\)
Latex block: $$e=mc^2$$
```

View File

@ -16,6 +16,14 @@ weight: 910
* **文件处理模型**:用于数据处理的【增强处理】和【问答拆分】。在【增强处理】中,生成相关问题和摘要,在【问答拆分】中执行问答对生成。 * **文件处理模型**:用于数据处理的【增强处理】和【问答拆分】。在【增强处理】中,生成相关问题和摘要,在【问答拆分】中执行问答对生成。
* **索引模型**:用于向量化,即通过对文本数据进行处理和组织,构建出一个能够快速查询的数据结构。 * **索引模型**:用于向量化,即通过对文本数据进行处理和组织,构建出一个能够快速查询的数据结构。
## 知识库支持Excel类文件的导入
xlsx等都可以上传的不止支持CSV。
## 知识库tokens的计算方式
统一按gpt3.5标准。
## 基于知识库的查询但是问题相关的答案过多。ai回答到一半就不继续回答。 ## 基于知识库的查询但是问题相关的答案过多。ai回答到一半就不继续回答。
FastGPT回复长度计算公式: FastGPT回复长度计算公式:
@ -37,7 +45,7 @@ FastGPT回复长度计算公式:
![](/imgs/dataset2.png) ![](/imgs/dataset2.png)
1. 私有化部署的时候,后台配模型参数,可以在配置最大上文时,预留一些空间,比如 128000 的模型,可以只配置 120000, 剩余的空间后续会被安排给输出 另外私有化部署的时候,后台配模型参数,可以在配置最大上文时,预留一些空间,比如 128000 的模型,可以只配置 120000, 剩余的空间后续会被安排给输出
## 受到模型上下文的限制,有时候达不到聊天记录的轮次,连续对话字数过多就会报上下文不够的错误。 ## 受到模型上下文的限制,有时候达不到聊天记录的轮次,连续对话字数过多就会报上下文不够的错误。
@ -61,4 +69,4 @@ FastGPT回复长度计算公式:
![](/imgs/dataset2.png) ![](/imgs/dataset2.png)
1. 私有化部署的时候,后台配模型参数,可以在配置最大上文时,预留一些空间,比如 128000 的模型,可以只配置 120000, 剩余的空间后续会被安排给输出 另外,私有化部署的时候,后台配模型参数,可以在配置最大上文时,预留一些空间,比如 128000 的模型,可以只配置 120000, 剩余的空间后续会被安排给输出

View File

@ -40,7 +40,7 @@ export type FastGPTConfigFileType = {
export type FastGPTFeConfigsType = { export type FastGPTFeConfigsType = {
show_emptyChat?: boolean; show_emptyChat?: boolean;
register_method?: ['email' | 'phone']; register_method?: ['email' | 'phone' | 'sync'];
login_method?: ['email' | 'phone']; // Attention: login method is diffrent with oauth login_method?: ['email' | 'phone']; // Attention: login method is diffrent with oauth
find_password_method?: ['email' | 'phone']; find_password_method?: ['email' | 'phone'];
bind_notification_method?: ['email' | 'phone']; bind_notification_method?: ['email' | 'phone'];
@ -76,7 +76,6 @@ export type FastGPTFeConfigsType = {
wecom?: { wecom?: {
corpid?: string; corpid?: string;
agentid?: string; agentid?: string;
secret?: string;
}; };
microsoft?: { microsoft?: {
clientId?: string; clientId?: string;

View File

@ -12,8 +12,9 @@ import { TeamTagSchema as TeamTagsSchemaType } from '@fastgpt/global/support/use
import { StoreEdgeItemType } from '../workflow/type/edge'; import { StoreEdgeItemType } from '../workflow/type/edge';
import { AppPermission } from '../../support/permission/app/controller'; import { AppPermission } from '../../support/permission/app/controller';
import { ParentIdType } from '../../common/parentFolder/type'; import { ParentIdType } from '../../common/parentFolder/type';
import { FlowNodeInputTypeEnum } from 'core/workflow/node/constant'; import { FlowNodeInputTypeEnum } from '../../core/workflow/node/constant';
import { WorkflowTemplateBasicType } from '@fastgpt/global/core/workflow/type'; import { WorkflowTemplateBasicType } from '@fastgpt/global/core/workflow/type';
import { SourceMemberType } from '../../support/user/type';
export type AppSchema = { export type AppSchema = {
_id: string; _id: string;
@ -63,6 +64,7 @@ export type AppListItemType = {
permission: AppPermission; permission: AppPermission;
inheritPermission?: boolean; inheritPermission?: boolean;
private?: boolean; private?: boolean;
sourceMember: SourceMemberType;
}; };
export type AppDetailType = AppSchema & { export type AppDetailType = AppSchema & {

View File

@ -1,5 +1,7 @@
import { TeamMemberStatusEnum } from 'support/user/team/constant';
import { StoreEdgeItemType } from '../workflow/type/edge'; import { StoreEdgeItemType } from '../workflow/type/edge';
import { AppChatConfigType, AppSchema } from './type'; import { AppChatConfigType, AppSchema } from './type';
import { SourceMemberType } from 'support/user/type';
export type AppVersionSchemaType = { export type AppVersionSchemaType = {
_id: string; _id: string;
@ -20,4 +22,5 @@ export type VersionListItemType = {
time: Date; time: Date;
isPublish: boolean | undefined; isPublish: boolean | undefined;
tmbId: string; tmbId: string;
sourceMember: SourceMemberType;
}; };

View File

@ -5,6 +5,7 @@ export type APIFileItem = {
type: 'file' | 'folder'; type: 'file' | 'folder';
updateTime: Date; updateTime: Date;
createTime: Date; createTime: Date;
hasChild?: boolean;
}; };
export type APIFileServer = { export type APIFileServer = {

View File

@ -11,6 +11,7 @@ import {
import { DatasetPermission } from '../../support/permission/dataset/controller'; import { DatasetPermission } from '../../support/permission/dataset/controller';
import { Permission } from '../../support/permission/controller'; import { Permission } from '../../support/permission/controller';
import { APIFileServer, FeishuServer, YuqueServer } from './apiDataset'; import { APIFileServer, FeishuServer, YuqueServer } from './apiDataset';
import { SourceMemberType } from 'support/user/type';
export type DatasetSchemaType = { export type DatasetSchemaType = {
_id: string; _id: string;
@ -165,6 +166,7 @@ export type DatasetListItemType = {
vectorModel: VectorModelItemType; vectorModel: VectorModelItemType;
inheritPermission: boolean; inheritPermission: boolean;
private?: boolean; private?: boolean;
sourceMember?: SourceMemberType;
}; };
export type DatasetItemType = Omit<DatasetSchemaType, 'vectorModel' | 'agentModel'> & { export type DatasetItemType = Omit<DatasetSchemaType, 'vectorModel' | 'agentModel'> & {

View File

@ -152,6 +152,7 @@ export enum NodeInputKeyEnum {
datasetSearchExtensionModel = 'datasetSearchExtensionModel', datasetSearchExtensionModel = 'datasetSearchExtensionModel',
datasetSearchExtensionBg = 'datasetSearchExtensionBg', datasetSearchExtensionBg = 'datasetSearchExtensionBg',
collectionFilterMatch = 'collectionFilterMatch', collectionFilterMatch = 'collectionFilterMatch',
authTmbId = 'authTmbId',
// concat dataset // concat dataset
datasetQuoteList = 'system_datasetQuoteList', datasetQuoteList = 'system_datasetQuoteList',

View File

@ -41,6 +41,10 @@ export type ChatDispatchProps = {
teamId: string; teamId: string;
tmbId: string; // App tmbId tmbId: string; // App tmbId
}; };
runningUserInfo: {
teamId: string;
tmbId: string;
};
uid: string; // Who run this workflow uid: string; // Who run this workflow
chatId?: string; chatId?: string;

View File

@ -89,6 +89,13 @@ export const DatasetSearchModule: FlowNodeTemplateType = {
valueType: WorkflowIOValueTypeEnum.string, valueType: WorkflowIOValueTypeEnum.string,
value: '' value: ''
}, },
{
key: NodeInputKeyEnum.authTmbId,
renderTypeList: [FlowNodeInputTypeEnum.hidden],
label: '',
valueType: WorkflowIOValueTypeEnum.boolean,
value: false
},
{ {
...Input_Template_UserChatInput, ...Input_Template_UserChatInput,
toolDescription: i18nT('workflow:content_to_search') toolDescription: i18nT('workflow:content_to_search')

View File

@ -89,6 +89,7 @@ export type NodeTemplateListItemType = {
hasTokenFee?: boolean; // 是否配置积分 hasTokenFee?: boolean; // 是否配置积分
instructions?: string; // 使用说明 instructions?: string; // 使用说明
courseUrl?: string; // 教程链接 courseUrl?: string; // 教程链接
sourceMember?: SourceMember;
}; };
export type NodeTemplateListType = { export type NodeTemplateListType = {

View File

@ -12,6 +12,7 @@ export type CreateTeamProps = {
avatar?: string; avatar?: string;
defaultTeam?: boolean; defaultTeam?: boolean;
memberName?: string; memberName?: string;
memberAvatar?: string;
}; };
export type UpdateTeamProps = Omit<ThirdPartyAccountType, 'externalWorkflowVariable'> & { export type UpdateTeamProps = Omit<ThirdPartyAccountType, 'externalWorkflowVariable'> & {
name?: string; name?: string;

View File

@ -5,8 +5,6 @@ export const OrgMemberCollectionName = 'team_org_members';
export const getOrgChildrenPath = (org: OrgSchemaType) => `${org.path}/${org.pathId}`; export const getOrgChildrenPath = (org: OrgSchemaType) => `${org.path}/${org.pathId}`;
// export enum OrgMemberRole { export enum SyncOrgSourceEnum {
// owner = 'owner', wecom = 'wecom'
// admin = 'admin', }
// member = 'member'
// }

View File

@ -13,6 +13,7 @@ type OrgSchemaType = {
}; };
type OrgMemberSchemaType = { type OrgMemberSchemaType = {
_id: string;
teamId: string; teamId: string;
orgId: string; orgId: string;
tmbId: string; tmbId: string;
@ -20,6 +21,6 @@ type OrgMemberSchemaType = {
type OrgType = Omit<OrgSchemaType, 'avatar'> & { type OrgType = Omit<OrgSchemaType, 'avatar'> & {
avatar: string; avatar: string;
members: OrgMemberSchemaType[];
permission: TeamPermission; permission: TeamPermission;
members: OrgMemberSchemaType[];
}; };

View File

@ -44,6 +44,7 @@ export type TeamMemberSchema = {
name: string; name: string;
role: `${TeamMemberRoleEnum}`; role: `${TeamMemberRoleEnum}`;
status: `${TeamMemberStatusEnum}`; status: `${TeamMemberStatusEnum}`;
avatar: string;
defaultTeam: boolean; defaultTeam: boolean;
}; };

View File

@ -1,12 +1,12 @@
import { TeamPermission } from '../permission/user/controller'; import { TeamPermission } from '../permission/user/controller';
import { UserStatusEnum } from './constant'; import { UserStatusEnum } from './constant';
import { TeamMemberStatusEnum } from './team/constant';
import { TeamTmbItemType } from './team/type'; import { TeamTmbItemType } from './team/type';
export type UserModelSchema = { export type UserModelSchema = {
_id: string; _id: string;
username: string; username: string;
password: string; password: string;
avatar: string;
promotionRate: number; promotionRate: number;
inviterId?: string; inviterId?: string;
openaiKey: string; openaiKey: string;
@ -22,7 +22,7 @@ export type UserModelSchema = {
export type UserType = { export type UserType = {
_id: string; _id: string;
username: string; username: string;
avatar: string; avatar: string; // it should be team member's avatar after 4.8.18
timezone: string; timezone: string;
promotionRate: UserModelSchema['promotionRate']; promotionRate: UserModelSchema['promotionRate'];
team: TeamTmbItemType; team: TeamTmbItemType;
@ -30,3 +30,9 @@ export type UserType = {
notificationAccount?: string; notificationAccount?: string;
permission: TeamPermission; permission: TeamPermission;
}; };
export type SourceMemberType = {
name: string;
avatar: string;
status: `${TeamMemberStatusEnum}`;
};

View File

@ -0,0 +1,21 @@
import { CommonErrEnum } from '@fastgpt/global/common/error/code/common';
import { ApiRequestProps } from '../../type/next';
export function parsePaginationRequest(req: ApiRequestProps) {
const {
pageSize = 10,
pageNum = 1,
offset = 0
} = Object.keys(req.body).includes('pageSize')
? req.body
: Object.keys(req.query).includes('pageSize')
? req.query
: {};
if (!pageSize || (pageNum === undefined && offset === undefined)) {
throw new Error(CommonErrEnum.missingParams);
}
return {
pageSize: Number(pageSize),
offset: offset ? Number(offset) : (Number(pageNum) - 1) * Number(pageSize)
};
}

View File

@ -2,7 +2,7 @@ import type { NextApiResponse, NextApiRequest } from 'next';
import NextCors from 'nextjs-cors'; import NextCors from 'nextjs-cors';
export async function withNextCors(req: NextApiRequest, res: NextApiResponse) { export async function withNextCors(req: NextApiRequest, res: NextApiResponse) {
const methods = ['GET', 'eHEAD', 'PUT', 'PATCH', 'POST', 'DELETE']; const methods = ['GET', 'HEAD', 'PUT', 'PATCH', 'POST', 'DELETE'];
const allowedOrigins = process.env.ALLOWED_ORIGINS?.split(','); const allowedOrigins = process.env.ALLOWED_ORIGINS?.split(',');
const origin = req.headers.origin; const origin = req.headers.origin;

View File

@ -99,7 +99,13 @@ export const useApiDatasetRequest = ({ apiServer }: { apiServer: APIFileServer }
if (files.some((file) => !file.id || !file.name || typeof file.type === 'undefined')) { if (files.some((file) => !file.id || !file.name || typeof file.type === 'undefined')) {
return Promise.reject('Invalid file data format'); return Promise.reject('Invalid file data format');
} }
return files;
const formattedFiles = files.map((file) => ({
...file,
hasChild: file.type === 'folder'
}));
return formattedFiles;
}; };
const getFileContent = async ({ teamId, apiFileId }: { teamId: string; apiFileId: string }) => { const getFileContent = async ({ teamId, apiFileId }: { teamId: string; apiFileId: string }) => {

View File

@ -284,7 +284,7 @@ export async function searchDatasetData(props: SearchDatasetDataProps) {
{ {
_id: { $in: collectionIdList } _id: { $in: collectionIdList }
}, },
'_id name fileId rawLink externalFileId externalFileUrl', '_id name fileId rawLink apiFileId externalFileId externalFileUrl',
{ ...readFromSecondary } { ...readFromSecondary }
).lean() ).lean()
]); ]);
@ -525,7 +525,7 @@ export async function searchDatasetData(props: SearchDatasetDataProps) {
{ {
_id: { $in: searchResults.map((item) => item.collectionId) } _id: { $in: searchResults.map((item) => item.collectionId) }
}, },
'_id name fileId rawLink externalFileId externalFileUrl', '_id name fileId rawLink apiFileId externalFileId externalFileUrl',
{ ...readFromSecondary } { ...readFromSecondary }
).lean() ).lean()
]); ]);

View File

@ -0,0 +1,39 @@
import { getTmbInfoByTmbId } from '../../support/user/team/controller';
import { getResourcePermission } from '../../support/permission/controller';
import { PerResourceTypeEnum } from '@fastgpt/global/support/permission/constant';
import { DatasetPermission } from '@fastgpt/global/support/permission/dataset/controller';
// TODO: 需要优化成批量获取权限
export const filterDatasetsByTmbId = async ({
datasetIds,
tmbId
}: {
datasetIds: string[];
tmbId: string;
}) => {
const { teamId, permission: tmbPer } = await getTmbInfoByTmbId({ tmbId });
// First get all permissions
const permissions = await Promise.all(
datasetIds.map(async (datasetId) => {
const per = await getResourcePermission({
teamId,
tmbId,
resourceId: datasetId,
resourceType: PerResourceTypeEnum.dataset
});
if (per === undefined) return false;
const datasetPer = new DatasetPermission({
per,
isOwner: tmbPer.isOwner
});
return datasetPer.hasReadPer;
})
);
// Then filter datasetIds based on permissions
return datasetIds.filter((_, index) => permissions[index]);
};

View File

@ -17,6 +17,7 @@ import { ChatNodeUsageType } from '@fastgpt/global/support/wallet/bill/type';
import { checkTeamReRankPermission } from '../../../../support/permission/teamLimit'; import { checkTeamReRankPermission } from '../../../../support/permission/teamLimit';
import { MongoDataset } from '../../../dataset/schema'; import { MongoDataset } from '../../../dataset/schema';
import { i18nT } from '../../../../../web/i18n/utils'; import { i18nT } from '../../../../../web/i18n/utils';
import { filterDatasetsByTmbId } from '../../../dataset/utils';
type DatasetSearchProps = ModuleDispatchProps<{ type DatasetSearchProps = ModuleDispatchProps<{
[NodeInputKeyEnum.datasetSelectList]: SelectedDatasetType; [NodeInputKeyEnum.datasetSelectList]: SelectedDatasetType;
@ -29,6 +30,7 @@ type DatasetSearchProps = ModuleDispatchProps<{
[NodeInputKeyEnum.datasetSearchExtensionModel]: string; [NodeInputKeyEnum.datasetSearchExtensionModel]: string;
[NodeInputKeyEnum.datasetSearchExtensionBg]: string; [NodeInputKeyEnum.datasetSearchExtensionBg]: string;
[NodeInputKeyEnum.collectionFilterMatch]: string; [NodeInputKeyEnum.collectionFilterMatch]: string;
[NodeInputKeyEnum.authTmbId]: boolean;
}>; }>;
export type DatasetSearchResponse = DispatchNodeResultType<{ export type DatasetSearchResponse = DispatchNodeResultType<{
[NodeOutputKeyEnum.datasetQuoteQA]: SearchDataResponseItemType[]; [NodeOutputKeyEnum.datasetQuoteQA]: SearchDataResponseItemType[];
@ -39,6 +41,7 @@ export async function dispatchDatasetSearch(
): Promise<DatasetSearchResponse> { ): Promise<DatasetSearchResponse> {
const { const {
runningAppInfo: { teamId }, runningAppInfo: { teamId },
runningUserInfo: { tmbId },
histories, histories,
node, node,
params: { params: {
@ -52,7 +55,8 @@ export async function dispatchDatasetSearch(
datasetSearchUsingExtensionQuery, datasetSearchUsingExtensionQuery,
datasetSearchExtensionModel, datasetSearchExtensionModel,
datasetSearchExtensionBg, datasetSearchExtensionBg,
collectionFilterMatch collectionFilterMatch,
authTmbId = false
} }
} = props as DatasetSearchProps; } = props as DatasetSearchProps;
@ -64,18 +68,20 @@ export async function dispatchDatasetSearch(
return Promise.reject(i18nT('common:core.chat.error.Select dataset empty')); return Promise.reject(i18nT('common:core.chat.error.Select dataset empty'));
} }
const emptyResult = {
quoteQA: [],
[DispatchNodeResponseKeyEnum.nodeResponse]: {
totalPoints: 0,
query: '',
limit,
searchMode
},
nodeDispatchUsages: [],
[DispatchNodeResponseKeyEnum.toolResponses]: []
};
if (!userChatInput) { if (!userChatInput) {
return { return emptyResult;
quoteQA: [],
[DispatchNodeResponseKeyEnum.nodeResponse]: {
totalPoints: 0,
query: '',
limit,
searchMode
},
nodeDispatchUsages: [],
[DispatchNodeResponseKeyEnum.toolResponses]: []
};
} }
// query extension // query extension
@ -83,13 +89,24 @@ export async function dispatchDatasetSearch(
? getLLMModel(datasetSearchExtensionModel) ? getLLMModel(datasetSearchExtensionModel)
: undefined; : undefined;
const { concatQueries, rewriteQuery, aiExtensionResult } = await datasetSearchQueryExtension({ const [{ concatQueries, rewriteQuery, aiExtensionResult }, datasetIds] = await Promise.all([
query: userChatInput, datasetSearchQueryExtension({
extensionModel, query: userChatInput,
extensionBg: datasetSearchExtensionBg, extensionModel,
histories: getHistories(6, histories) extensionBg: datasetSearchExtensionBg,
}); histories: getHistories(6, histories)
}),
authTmbId
? filterDatasetsByTmbId({
datasetIds: datasets.map((item) => item.datasetId),
tmbId
})
: Promise.resolve(datasets.map((item) => item.datasetId))
]);
if (datasetIds.length === 0) {
return emptyResult;
}
// console.log(concatQueries, rewriteQuery, aiExtensionResult); // console.log(concatQueries, rewriteQuery, aiExtensionResult);
// get vector // get vector
@ -110,7 +127,7 @@ export async function dispatchDatasetSearch(
model: vectorModel.model, model: vectorModel.model,
similarity, similarity,
limit, limit,
datasetIds: datasets.map((item) => item.datasetId), datasetIds,
searchMode, searchMode,
usingReRank: usingReRank && (await checkTeamReRankPermission(teamId)), usingReRank: usingReRank && (await checkTeamReRankPermission(teamId)),
collectionFilterMatch collectionFilterMatch

View File

@ -17,7 +17,6 @@ import { ParentIdType } from '@fastgpt/global/common/parentFolder/type';
import { CommonErrEnum } from '@fastgpt/global/common/error/code/common'; import { CommonErrEnum } from '@fastgpt/global/common/error/code/common';
import { MemberGroupSchemaType } from '@fastgpt/global/support/permission/memberGroup/type'; import { MemberGroupSchemaType } from '@fastgpt/global/support/permission/memberGroup/type';
import { TeamMemberSchema } from '@fastgpt/global/support/user/team/type'; import { TeamMemberSchema } from '@fastgpt/global/support/user/team/type';
import { UserModelSchema } from '@fastgpt/global/support/user/type';
import { OrgSchemaType } from '@fastgpt/global/support/user/team/org/type'; import { OrgSchemaType } from '@fastgpt/global/support/user/team/org/type';
import { getOrgIdSetWithParentByTmbId } from './org/controllers'; import { getOrgIdSetWithParentByTmbId } from './org/controllers';
@ -151,13 +150,9 @@ export const getClbsAndGroupsWithInfo = async ({
$exists: true $exists: true
} }
}) })
.populate<{ tmb: TeamMemberSchema & { user: UserModelSchema } }>({ .populate<{ tmb: TeamMemberSchema }>({
path: 'tmb', path: 'tmb',
select: 'name userId role', select: 'name userId avatar'
populate: {
path: 'user',
select: 'avatar'
}
}) })
.lean(), .lean(),
MongoResourcePermission.find({ MongoResourcePermission.find({

View File

@ -47,14 +47,11 @@ export const OrgSchema = new Schema(
OrgSchema.virtual('members', { OrgSchema.virtual('members', {
ref: OrgMemberCollectionName, ref: OrgMemberCollectionName,
localField: '_id', localField: '_id',
foreignField: 'orgId' foreignField: 'orgId',
match: function (this: OrgSchemaType) {
return { teamId: this.teamId };
}
}); });
// OrgSchema.virtual('permission', {
// ref: ResourcePermissionCollectionName,
// localField: '_id',
// foreignField: 'orgId',
// justOne: true
// });
try { try {
OrgSchema.index({ OrgSchema.index({

View File

@ -41,7 +41,7 @@ export async function getUserDetail({
return { return {
_id: user._id, _id: user._id,
username: user.username, username: user.username,
avatar: user.avatar, avatar: tmb.avatar,
timezone: user.timezone, timezone: user.timezone,
promotionRate: user.promotionRate, promotionRate: user.promotionRate,
team: tmb, team: tmb,

View File

@ -3,7 +3,6 @@ const { Schema } = connectionMongo;
import { hashStr } from '@fastgpt/global/common/string/tools'; import { hashStr } from '@fastgpt/global/common/string/tools';
import type { UserModelSchema } from '@fastgpt/global/support/user/type'; import type { UserModelSchema } from '@fastgpt/global/support/user/type';
import { UserStatusEnum, userStatusMap } from '@fastgpt/global/support/user/constant'; import { UserStatusEnum, userStatusMap } from '@fastgpt/global/support/user/constant';
import { getRandomUserAvatar } from '@fastgpt/global/support/user/utils';
export const userCollectionName = 'users'; export const userCollectionName = 'users';
@ -33,11 +32,6 @@ const UserSchema = new Schema({
type: Date, type: Date,
default: () => new Date() default: () => new Date()
}, },
avatar: {
type: String,
default: () => getRandomUserAvatar()
},
promotionRate: { promotionRate: {
type: Number, type: Number,
default: 15 default: 15
@ -62,7 +56,10 @@ const UserSchema = new Schema({
ref: userCollectionName ref: userCollectionName
}, },
fastgpt_sem: Object, fastgpt_sem: Object,
sourceDomain: String sourceDomain: String,
/** @deprecated */
avatar: String
}); });
try { try {

View File

@ -37,7 +37,7 @@ async function getTeamMember(match: Record<string, any>): Promise<TeamTmbItemTyp
teamAvatar: tmb.team.avatar, teamAvatar: tmb.team.avatar,
teamName: tmb.team.name, teamName: tmb.team.name,
memberName: tmb.name, memberName: tmb.name,
avatar: tmb.team.avatar, avatar: tmb.avatar,
balance: tmb.team.balance, balance: tmb.team.balance,
tmbId: String(tmb._id), tmbId: String(tmb._id),
teamDomain: tmb.team?.teamDomain, teamDomain: tmb.team?.teamDomain,

View File

@ -7,6 +7,7 @@ import {
TeamMemberCollectionName, TeamMemberCollectionName,
TeamCollectionName TeamCollectionName
} from '@fastgpt/global/support/user/team/constant'; } from '@fastgpt/global/support/user/team/constant';
import { getRandomUserAvatar } from '@fastgpt/global/support/user/utils';
const TeamMemberSchema = new Schema({ const TeamMemberSchema = new Schema({
teamId: { teamId: {
@ -19,6 +20,10 @@ const TeamMemberSchema = new Schema({
ref: userCollectionName, ref: userCollectionName,
required: true required: true
}, },
avatar: {
type: String,
default: () => getRandomUserAvatar()
},
name: { name: {
type: String, type: String,
default: 'Member' default: 'Member'
@ -39,7 +44,6 @@ const TeamMemberSchema = new Schema({
// Abandoned // Abandoned
role: { role: {
type: String type: String
// enum: Object.keys(TeamMemberRoleMap) // disable enum validation for old data
} }
}); });

View File

@ -1,4 +1,7 @@
import { SourceMemberType } from '@fastgpt/global/support/user/type';
import { MongoTeam } from './team/teamSchema'; import { MongoTeam } from './team/teamSchema';
import { MongoTeamMember } from './team/teamMemberSchema';
import { ClientSession } from '../../common/mongo';
/* export dataset limit */ /* export dataset limit */
export const updateExportDatasetLimit = async (teamId: string) => { export const updateExportDatasetLimit = async (teamId: string) => {
@ -67,3 +70,41 @@ export const checkWebSyncLimit = async ({
return Promise.reject(`每个团队,每 ${limitMinutes} 分钟仅使用一次同步功能。`); return Promise.reject(`每个团队,每 ${limitMinutes} 分钟仅使用一次同步功能。`);
} }
}; };
/**
* This function will add a property named sourceMember to the list passed in.
* @param list The list to add the sourceMember property to. [TmbId] property is required.
* @error If member is not found, this item will be skipped.
* @returns The list with the sourceMember property added.
*/
export async function addSourceMember<T extends { tmbId: string }>({
list,
session
}: {
list: T[];
session?: ClientSession;
}): Promise<Array<T & { sourceMember: SourceMemberType }>> {
if (!Array.isArray(list)) return [];
const tmbList = await MongoTeamMember.find(
{
_id: { $in: list.map((item) => String(item.tmbId)) }
},
'tmbId name avatar status',
{
session
}
).lean();
return list
.map((item) => {
const tmb = tmbList.find((tmb) => String(tmb._id) === String(item.tmbId));
if (!tmb) return;
return {
...item,
sourceMember: { name: tmb.name, avatar: tmb.avatar, status: tmb.status }
};
})
.filter(Boolean) as Array<T & { sourceMember: SourceMemberType }>;
}

View File

@ -1,8 +1,13 @@
export type PaginationProps<T = {}> = T & { import { RequireOnlyOne } from '@fastgpt/global/common/type/utils';
offset: number;
pageSize: number; type PaginationProps<T = {}> = T & {
}; pageSize: number | string;
export type PaginationResponse<T = any> = { } & RequireOnlyOne<{
offset: number | string;
pageNum: number | string;
}>;
type PaginationResponse<T = {}> = {
total: number; total: number;
list: T[]; list: T[];
}; };

View File

@ -23,7 +23,7 @@ function AvatarGroup({
<Flex position="relative"> <Flex position="relative">
{avatars.slice(0, max).map((avatar, index) => ( {avatars.slice(0, max).map((avatar, index) => (
<Avatar <Avatar
key={avatar + groupId} key={index}
src={avatar} src={avatar}
position={index > 0 ? 'absolute' : 'relative'} position={index > 0 ? 'absolute' : 'relative'}
left={index > 0 ? `${index * 15}px` : 0} left={index > 0 ? `${index * 15}px` : 0}

View File

@ -21,7 +21,15 @@ import type { ButtonProps, MenuItemProps } from '@chakra-ui/react';
import MyIcon from '../Icon'; import MyIcon from '../Icon';
import { useRequest2 } from '../../../hooks/useRequest'; import { useRequest2 } from '../../../hooks/useRequest';
import MyDivider from '../MyDivider'; import MyDivider from '../MyDivider';
import { useScrollPagination } from '../../../hooks/useScrollPagination';
/** Props
* value: 选中的值
* placeholder: 占位符
* list: 列表数据
* isLoading: 是否加载中
* ScrollData: 分页滚动数据控制器 [useScrollPagination]
* */
export type SelectProps<T = any> = ButtonProps & { export type SelectProps<T = any> = ButtonProps & {
value?: T; value?: T;
placeholder?: string; placeholder?: string;
@ -34,6 +42,7 @@ export type SelectProps<T = any> = ButtonProps & {
}[]; }[];
isLoading?: boolean; isLoading?: boolean;
onchange?: (val: T) => any | Promise<any>; onchange?: (val: T) => any | Promise<any>;
ScrollData?: ReturnType<typeof useScrollPagination>['ScrollData'];
}; };
const MySelect = <T = any,>( const MySelect = <T = any,>(
@ -44,6 +53,7 @@ const MySelect = <T = any,>(
list = [], list = [],
onchange, onchange,
isLoading = false, isLoading = false,
ScrollData,
...props ...props
}: SelectProps<T>, }: SelectProps<T>,
ref: ForwardedRef<{ ref: ForwardedRef<{
@ -87,6 +97,46 @@ const MySelect = <T = any,>(
const isSelecting = loading || isLoading; const isSelecting = loading || isLoading;
const ListRender = useMemo(() => {
return (
<>
{list.map((item, i) => (
<Box key={i}>
<MenuItem
{...menuItemStyles}
{...(value === item.value
? {
ref: SelectedItemRef,
color: 'primary.700',
bg: 'myGray.100',
fontWeight: '600'
}
: {
color: 'myGray.900'
})}
onClick={() => {
if (onChange && value !== item.value) {
onChange(item.value);
}
}}
whiteSpace={'pre-wrap'}
fontSize={'sm'}
display={'block'}
>
<Box>{item.label}</Box>
{item.description && (
<Box color={'myGray.500'} fontSize={'xs'}>
{item.description}
</Box>
)}
</MenuItem>
{item.showBorder && <MyDivider my={2} />}
</Box>
))}
</>
);
}, []);
return ( return (
<Box <Box
css={css({ css={css({
@ -154,39 +204,7 @@ const MySelect = <T = any,>(
maxH={'40vh'} maxH={'40vh'}
overflowY={'auto'} overflowY={'auto'}
> >
{list.map((item, i) => ( {ScrollData ? <ScrollData>{ListRender}</ScrollData> : ListRender}
<Box key={i}>
<MenuItem
{...menuItemStyles}
{...(value === item.value
? {
ref: SelectedItemRef,
color: 'primary.700',
bg: 'myGray.100',
fontWeight: '600'
}
: {
color: 'myGray.900'
})}
onClick={() => {
if (onChange && value !== item.value) {
onChange(item.value);
}
}}
whiteSpace={'pre-wrap'}
fontSize={'sm'}
display={'block'}
>
<Box>{item.label}</Box>
{item.description && (
<Box color={'myGray.500'} fontSize={'xs'}>
{item.description}
</Box>
)}
</MenuItem>
{item.showBorder && <MyDivider my={2} />}
</Box>
))}
</MenuList> </MenuList>
</Menu> </Menu>
</Box> </Box>

View File

@ -0,0 +1,23 @@
import { Box, HStack, type StackProps } from '@chakra-ui/react';
import { SourceMemberType } from '@fastgpt/global/support/user/type';
import React from 'react';
import Avatar from '../Avatar';
import { useTranslation } from 'next-i18next';
import Tag from '../Tag';
export type UserBoxProps = {
sourceMember: SourceMemberType;
avatarSize?: string;
} & StackProps;
function UserBox({ sourceMember, avatarSize = '1.25rem', ...props }: UserBoxProps) {
const { t } = useTranslation();
return (
<HStack space="1" {...props}>
<Avatar src={sourceMember.avatar} w={avatarSize} />
<Box>{sourceMember.name}</Box>
{sourceMember.status === 'leave' && <Tag color="gray">{t('common:user_leaved')}</Tag>}
</HStack>
);
}
export default React.memo(UserBox);

View File

@ -4,7 +4,6 @@ import { ArrowBackIcon, ArrowForwardIcon } from '@chakra-ui/icons';
import { useTranslation } from 'next-i18next'; import { useTranslation } from 'next-i18next';
import { useToast } from './useToast'; import { useToast } from './useToast';
import { getErrText } from '@fastgpt/global/common/error/utils'; import { getErrText } from '@fastgpt/global/common/error/utils';
import { import {
useBoolean, useBoolean,
useLockFn, useLockFn,
@ -14,37 +13,33 @@ import {
useThrottleEffect useThrottleEffect
} from 'ahooks'; } from 'ahooks';
import { PaginationProps, PaginationResponse } from '../common/fetch/type';
const thresholdVal = 200; const thresholdVal = 200;
type PagingData<T> = { export function usePagination<DataT, ResT = {}>(
pageNum: number; api: (data: PaginationProps<DataT>) => Promise<PaginationResponse<ResT>>,
pageSize: number; {
data: T[]; pageSize = 10,
total?: number; params,
}; defaultRequest = true,
type = 'button',
export function usePagination<ResT = any>({ onChange,
api, refreshDeps,
pageSize = 10, scrollLoadType = 'bottom',
params = {}, EmptyTip
defaultRequest = true, }: {
type = 'button', pageSize?: number;
onChange, params?: DataT;
refreshDeps, defaultRequest?: boolean;
scrollLoadType = 'bottom', type?: 'button' | 'scroll';
EmptyTip onChange?: (pageNum: number) => void;
}: { refreshDeps?: any[];
api: (data: any) => Promise<PagingData<ResT>>; throttleWait?: number;
pageSize?: number; scrollLoadType?: 'top' | 'bottom';
params?: Record<string, any>; EmptyTip?: React.JSX.Element;
defaultRequest?: boolean; }
type?: 'button' | 'scroll'; ) {
onChange?: (pageNum: number) => void;
refreshDeps?: any[];
throttleWait?: number;
scrollLoadType?: 'top' | 'bottom';
EmptyTip?: React.JSX.Element;
}) {
const { toast } = useToast(); const { toast } = useToast();
const { t } = useTranslation(); const { t } = useTranslation();
@ -64,7 +59,7 @@ export function usePagination<ResT = any>({
setTrue(); setTrue();
try { try {
const res: PagingData<ResT> = await api({ const res = await api({
pageNum: num, pageNum: num,
pageSize, pageSize,
...params ...params
@ -93,13 +88,13 @@ export function usePagination<ResT = any>({
); );
} }
setData((prevData) => (num === 1 ? res.data : [...res.data, ...prevData])); setData((prevData) => (num === 1 ? res.list : [...res.list, ...prevData]));
adjustScrollPosition(); adjustScrollPosition();
} else { } else {
setData((prevData) => (num === 1 ? res.data : [...prevData, ...res.data])); setData((prevData) => (num === 1 ? res.list : [...prevData, ...res.list]));
} }
} else { } else {
setData(res.data); setData(res.list);
} }
onChange?.(num); onChange?.(num);

View File

@ -16,7 +16,7 @@ import MyBox from '../components/common/MyBox';
import { useTranslation } from 'next-i18next'; import { useTranslation } from 'next-i18next';
type ItemHeight<T> = (index: number, data: T) => number; type ItemHeight<T> = (index: number, data: T) => number;
const thresholdVal = 200; const thresholdVal = 100;
export type ScrollListType = ({ export type ScrollListType = ({
children, children,
@ -269,8 +269,10 @@ export function useScrollPagination<
({ ({
children, children,
ScrollContainerRef, ScrollContainerRef,
isLoading,
...props ...props
}: { }: {
isLoading?: boolean;
children: ReactNode; children: ReactNode;
ScrollContainerRef?: RefObject<HTMLDivElement>; ScrollContainerRef?: RefObject<HTMLDivElement>;
} & BoxProps) => { } & BoxProps) => {
@ -302,7 +304,7 @@ export function useScrollPagination<
); );
return ( return (
<Box {...props} ref={ref} overflow={'overlay'}> <MyBox {...props} ref={ref} overflow={'overlay'} isLoading={isLoading}>
{scrollLoadType === 'top' && total > 0 && isLoading && ( {scrollLoadType === 'top' && total > 0 && isLoading && (
<Box mt={2} fontSize={'xs'} color={'blackAlpha.500'} textAlign={'center'}> <Box mt={2} fontSize={'xs'} color={'blackAlpha.500'} textAlign={'center'}>
{t('common:common.is_requesting')} {t('common:common.is_requesting')}
@ -325,7 +327,7 @@ export function useScrollPagination<
</Box> </Box>
)} )}
{isEmpty && EmptyTip} {isEmpty && EmptyTip}
</Box> </MyBox>
); );
} }
); );

View File

@ -39,6 +39,7 @@
"classification": "Classification", "classification": "Classification",
"click_to_resume": "Click to Resume", "click_to_resume": "Click to Resume",
"code_editor": "Code Editor", "code_editor": "Code Editor",
"code_error.account_error": "Incorrect account name or password",
"code_error.app_error.invalid_app_type": "Invalid Application Type", "code_error.app_error.invalid_app_type": "Invalid Application Type",
"code_error.app_error.invalid_owner": "Unauthorized Application Owner", "code_error.app_error.invalid_owner": "Unauthorized Application Owner",
"code_error.app_error.not_exist": "Application Does Not Exist", "code_error.app_error.not_exist": "Application Does Not Exist",
@ -95,7 +96,6 @@
"code_error.team_error.website_sync_not_enough": "Unauthorized to Use Website Sync", "code_error.team_error.website_sync_not_enough": "Unauthorized to Use Website Sync",
"code_error.token_error_code.403": "Invalid Login Status, Please Re-login", "code_error.token_error_code.403": "Invalid Login Status, Please Re-login",
"code_error.user_error.balance_not_enough": "Insufficient Account Balance", "code_error.user_error.balance_not_enough": "Insufficient Account Balance",
"code_error.account_error": "Incorrect account name or password",
"code_error.user_error.bin_visitor_guest": "You Are Currently a Guest, Unauthorized to Operate", "code_error.user_error.bin_visitor_guest": "You Are Currently a Guest, Unauthorized to Operate",
"code_error.user_error.un_auth_user": "User Not Found", "code_error.user_error.un_auth_user": "User Not Found",
"common.Action": "Action", "common.Action": "Action",
@ -1273,6 +1273,7 @@
"user.team.role.Visitor": "visitor", "user.team.role.Visitor": "visitor",
"user.team.role.writer": "writable member", "user.team.role.writer": "writable member",
"user.type": "Type", "user.type": "Type",
"user_leaved": "Leaved",
"verification": "Verification", "verification": "Verification",
"workflow.template.communication": "Communication", "workflow.template.communication": "Communication",
"xx_search_result": "{{key}} Search Results", "xx_search_result": "{{key}} Search Results",

View File

@ -13,6 +13,8 @@
"append_application_reply_to_history_as_new_context": "Append the application's reply to the history as new context", "append_application_reply_to_history_as_new_context": "Append the application's reply to the history as new context",
"application_call": "Application Call", "application_call": "Application Call",
"assigned_reply": "Assigned Reply", "assigned_reply": "Assigned Reply",
"auth_tmb_id": "Auth member",
"auth_tmb_id_tip": "After it is turned on, when the application is released to the outside world, the knowledge base will be filtered based on whether the user has permission to the knowledge base.\n\nIf it is not enabled, the configured knowledge base will be searched directly without permission filtering.",
"can_not_loop": "This node can't loop.", "can_not_loop": "This node can't loop.",
"choose_another_application_to_call": "Select another application to call", "choose_another_application_to_call": "Select another application to call",
"classification_result": "Classification Result", "classification_result": "Classification Result",

View File

@ -35,5 +35,8 @@
"user_team_invite_member": "邀请成员", "user_team_invite_member": "邀请成员",
"user_team_leave_team": "离开团队", "user_team_leave_team": "离开团队",
"user_team_leave_team_failed": "离开团队失败", "user_team_leave_team_failed": "离开团队失败",
"waiting": "待接受" "waiting": "待接受",
"sync_immediately": "立即同步",
"sync_member_failed": "同步成员失败",
"sync_member_success": "同步成员成功"
} }

View File

@ -43,6 +43,7 @@
"classification": "分类", "classification": "分类",
"click_to_resume": "点击恢复", "click_to_resume": "点击恢复",
"code_editor": "代码编辑", "code_editor": "代码编辑",
"code_error.account_error": "账号名或密码错误",
"code_error.app_error.invalid_app_type": "错误的应用类型", "code_error.app_error.invalid_app_type": "错误的应用类型",
"code_error.app_error.invalid_owner": "非法的应用所有者", "code_error.app_error.invalid_owner": "非法的应用所有者",
"code_error.app_error.not_exist": "应用不存在", "code_error.app_error.not_exist": "应用不存在",
@ -99,7 +100,6 @@
"code_error.team_error.website_sync_not_enough": "无权使用Web站点同步~", "code_error.team_error.website_sync_not_enough": "无权使用Web站点同步~",
"code_error.token_error_code.403": "登录状态无效,请重新登录", "code_error.token_error_code.403": "登录状态无效,请重新登录",
"code_error.user_error.balance_not_enough": "账号余额不足~", "code_error.user_error.balance_not_enough": "账号余额不足~",
"code_error.account_error": "账号名或密码错误",
"code_error.user_error.bin_visitor_guest": "您当前身份为游客,无权操作", "code_error.user_error.bin_visitor_guest": "您当前身份为游客,无权操作",
"code_error.user_error.un_auth_user": "找不到该用户", "code_error.user_error.un_auth_user": "找不到该用户",
"common.Action": "操作", "common.Action": "操作",
@ -1268,6 +1268,7 @@
"user.team.role.Visitor": "访客", "user.team.role.Visitor": "访客",
"user.team.role.writer": "可写成员", "user.team.role.writer": "可写成员",
"user.type": "类型", "user.type": "类型",
"user_leaved": "已离开",
"verification": "验证", "verification": "验证",
"workflow.template.communication": "通信", "workflow.template.communication": "通信",
"xx_search_result": "{{key}} 的搜索结果", "xx_search_result": "{{key}} 的搜索结果",

View File

@ -13,6 +13,8 @@
"append_application_reply_to_history_as_new_context": "将该应用回复内容拼接到历史记录中,作为新的上下文返回", "append_application_reply_to_history_as_new_context": "将该应用回复内容拼接到历史记录中,作为新的上下文返回",
"application_call": "应用调用", "application_call": "应用调用",
"assigned_reply": "指定回复", "assigned_reply": "指定回复",
"auth_tmb_id": "使用者鉴权",
"auth_tmb_id_tip": "开启后,对外发布该应用时,还会根据用户是否有该知识库权限进行知识库过滤。\n若未开启则直接按配置的知识库进行检索不进行权限过滤。",
"can_not_loop": "该节点不支持循环嵌套", "can_not_loop": "该节点不支持循环嵌套",
"choose_another_application_to_call": "选择一个其他应用进行调用", "choose_another_application_to_call": "选择一个其他应用进行调用",
"classification_result": "分类结果", "classification_result": "分类结果",

View File

@ -39,6 +39,7 @@
"classification": "分類", "classification": "分類",
"click_to_resume": "點選繼續", "click_to_resume": "點選繼續",
"code_editor": "程式碼編輯器", "code_editor": "程式碼編輯器",
"code_error.account_error": "帳號名稱或密碼錯誤",
"code_error.app_error.invalid_app_type": "無效的應用程式類型", "code_error.app_error.invalid_app_type": "無效的應用程式類型",
"code_error.app_error.invalid_owner": "非法的應用程式擁有者", "code_error.app_error.invalid_owner": "非法的應用程式擁有者",
"code_error.app_error.not_exist": "應用程式不存在", "code_error.app_error.not_exist": "應用程式不存在",
@ -95,7 +96,6 @@
"code_error.team_error.website_sync_not_enough": "無權使用網站同步", "code_error.team_error.website_sync_not_enough": "無權使用網站同步",
"code_error.token_error_code.403": "登入狀態無效,請重新登入", "code_error.token_error_code.403": "登入狀態無效,請重新登入",
"code_error.user_error.balance_not_enough": "帳戶餘額不足", "code_error.user_error.balance_not_enough": "帳戶餘額不足",
"code_error.account_error": "帳號名稱或密碼錯誤",
"code_error.user_error.bin_visitor_guest": "您目前身份為訪客,無權操作", "code_error.user_error.bin_visitor_guest": "您目前身份為訪客,無權操作",
"code_error.user_error.un_auth_user": "找不到此使用者", "code_error.user_error.un_auth_user": "找不到此使用者",
"common.Action": "操作", "common.Action": "操作",
@ -1273,6 +1273,7 @@
"user.team.role.Visitor": "訪客", "user.team.role.Visitor": "訪客",
"user.team.role.writer": "可寫入成員", "user.team.role.writer": "可寫入成員",
"user.type": "類型", "user.type": "類型",
"user_leaved": "已離開",
"verification": "驗證", "verification": "驗證",
"workflow.template.communication": "通訊", "workflow.template.communication": "通訊",
"xx_search_result": "{{key}} 的搜尋結果", "xx_search_result": "{{key}} 的搜尋結果",

View File

@ -13,6 +13,8 @@
"append_application_reply_to_history_as_new_context": "將應用程式的回覆附加到歷史紀錄中,作為新的脈絡", "append_application_reply_to_history_as_new_context": "將應用程式的回覆附加到歷史紀錄中,作為新的脈絡",
"application_call": "應用程式呼叫", "application_call": "應用程式呼叫",
"assigned_reply": "指定回覆", "assigned_reply": "指定回覆",
"auth_tmb_id": "使用者鑑權",
"auth_tmb_id_tip": "開啟後,對外發布應用程式時,也會根據使用者是否有該知識庫權限進行知識庫過濾。\n\n若未開啟則直接按配置的知識庫進行檢索不進行權限過濾。",
"can_not_loop": "這個節點不能迴圈。", "can_not_loop": "這個節點不能迴圈。",
"choose_another_application_to_call": "選擇另一個應用程式來呼叫", "choose_another_application_to_call": "選擇另一個應用程式來呼叫",
"classification_result": "分類結果", "classification_result": "分類結果",

View File

@ -57,3 +57,5 @@ WORKFLOW_MAX_LOOP_TIMES=50
# CHAT_LOG_INTERVAL=10000 # CHAT_LOG_INTERVAL=10000
# # 日志来源ID前缀 # # 日志来源ID前缀
# CHAT_LOG_SOURCE_ID_PREFIX=fastgpt- # CHAT_LOG_SOURCE_ID_PREFIX=fastgpt-
# 自定义跨域,不配置时,默认都允许跨域(逗号分割)
ALLOWED_ORIGINS=

View File

@ -1,6 +1,6 @@
{ {
"name": "app", "name": "app",
"version": "4.8.18", "version": "4.8.19",
"private": false, "private": false,
"scripts": { "scripts": {
"dev": "next dev", "dev": "next dev",

View File

@ -54,7 +54,7 @@ const InputGuideConfig = ({
onChange: (e: ChatInputGuideConfigType) => void; onChange: (e: ChatInputGuideConfigType) => void;
}) => { }) => {
const { t } = useTranslation(); const { t } = useTranslation();
const { chatT, commonT } = useI18n(); const { chatT } = useI18n();
const { isOpen, onOpen, onClose } = useDisclosure(); const { isOpen, onOpen, onClose } = useDisclosure();
const { const {
isOpen: isOpenLexiconConfig, isOpen: isOpenLexiconConfig,
@ -220,7 +220,7 @@ const LexiconConfigModal = ({ appId, onClose }: { appId: string; onClose: () =>
}); });
const { run: createNewData, loading: isCreating } = useRequest2( const { run: createNewData, loading: isCreating } = useRequest2(
(textList: string[]) => { async (textList: string[]) => {
if (textList.filter(Boolean).length === 0) { if (textList.filter(Boolean).length === 0) {
return Promise.resolve(); return Promise.resolve();
} }

View File

@ -5,7 +5,6 @@ import { useTranslation } from 'next-i18next';
import { getCollectionSourceAndOpen } from '@/web/core/dataset/hooks/readCollectionSource'; import { getCollectionSourceAndOpen } from '@/web/core/dataset/hooks/readCollectionSource';
import { getSourceNameIcon } from '@fastgpt/global/core/dataset/utils'; import { getSourceNameIcon } from '@fastgpt/global/core/dataset/utils';
import MyIcon from '@fastgpt/web/components/common/Icon'; import MyIcon from '@fastgpt/web/components/common/Icon';
import { useI18n } from '@/web/context/I18n';
import type { readCollectionSourceBody } from '@/pages/api/core/dataset/collection/read'; import type { readCollectionSourceBody } from '@/pages/api/core/dataset/collection/read';
type Props = BoxProps & type Props = BoxProps &
@ -33,7 +32,6 @@ const RawSourceBox = ({
...props ...props
}: Props) => { }: Props) => {
const { t } = useTranslation(); const { t } = useTranslation();
const { fileT } = useI18n();
const canPreview = !!sourceId && canView; const canPreview = !!sourceId && canView;
@ -51,7 +49,7 @@ const RawSourceBox = ({
return ( return (
<MyTooltip <MyTooltip
label={canPreview ? fileT('click_to_view_raw_source') : ''} label={canPreview ? t('file:click_to_view_raw_source') : ''}
shouldWrapChildren={false} shouldWrapChildren={false}
> >
<Box <Box

View File

@ -1,4 +1,4 @@
import { useUserStore } from '@/web/support/user/useUserStore'; import { getTeamMembers } from '@/web/support/user/team/api';
import { import {
Box, Box,
Flex, Flex,
@ -15,6 +15,7 @@ import Icon from '@fastgpt/web/components/common/Icon';
import MyModal from '@fastgpt/web/components/common/MyModal'; import MyModal from '@fastgpt/web/components/common/MyModal';
import MyTag from '@fastgpt/web/components/common/Tag'; import MyTag from '@fastgpt/web/components/common/Tag';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest'; import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { useScrollPagination } from '@fastgpt/web/hooks/useScrollPagination';
import { useTranslation } from 'next-i18next'; import { useTranslation } from 'next-i18next';
import React, { useState } from 'react'; import React, { useState } from 'react';
@ -31,13 +32,12 @@ export function ChangeOwnerModal({
onChangeOwner onChangeOwner
}: ChangeOwnerModalProps & { onClose: () => void }) { }: ChangeOwnerModalProps & { onClose: () => void }) {
const { t } = useTranslation(); const { t } = useTranslation();
const { loadAndGetTeamMembers } = useUserStore();
const [inputValue, setInputValue] = React.useState(''); const [inputValue, setInputValue] = React.useState('');
const { data: teamMembers = [] } = useRequest2(loadAndGetTeamMembers, { const { data: teamMembers, ScrollData } = useScrollPagination(getTeamMembers, {
manual: false pageSize: 15
}); });
const memberList = teamMembers.filter((item) => { const memberList = teamMembers.filter((item) => {
return item.memberName.includes(inputValue); return item.memberName.includes(inputValue);
}); });
@ -101,11 +101,6 @@ export function ChangeOwnerModal({
onOpenMemberListMenu(); onOpenMemberListMenu();
setSelectedMember(null); setSelectedMember(null);
}} }}
// onBlur={() => {
// setTimeout(() => {
// onCloseMemberListMenu();
// }, 10);
// }}
{...(selectedMember && { pl: '10' })} {...(selectedMember && { pl: '10' })}
/> />
</Flex> </Flex>
@ -123,26 +118,28 @@ export function ChangeOwnerModal({
maxH={'300px'} maxH={'300px'}
overflow={'auto'} overflow={'auto'}
> >
{memberList.map((item) => ( <ScrollData>
<Box {memberList.map((item) => (
key={item.tmbId} <Box
p="2" key={item.tmbId}
_hover={{ bg: 'myGray.100' }} p="2"
mx="1" _hover={{ bg: 'myGray.100' }}
borderRadius="md" mx="1"
cursor={'pointer'} borderRadius="md"
onClickCapture={() => { cursor={'pointer'}
setInputValue(item.memberName); onClickCapture={() => {
setSelectedMember(item); setInputValue(item.memberName);
onCloseMemberListMenu(); setSelectedMember(item);
}} onCloseMemberListMenu();
> }}
<Flex align="center"> >
<Avatar src={item.avatar} w="1.25rem" /> <Flex align="center">
<Box ml="2">{item.memberName}</Box> <Avatar src={item.avatar} w="1.25rem" />
</Flex> <Box ml="2">{item.memberName}</Box>
</Box> </Flex>
))} </Box>
))}
</ScrollData>
</Flex> </Flex>
)} )}

View File

@ -33,6 +33,10 @@ import { ParentTreePathItemType } from '@fastgpt/global/common/parentFolder/type
import { OrgType } from '@fastgpt/global/support/user/team/org/type'; import { OrgType } from '@fastgpt/global/support/user/team/org/type';
import { useContextSelector } from 'use-context-selector'; import { useContextSelector } from 'use-context-selector';
import { CollaboratorContext } from './context'; import { CollaboratorContext } from './context';
import { getTeamMembers } from '@/web/support/user/team/api';
import { getGroupList } from '@/web/support/user/team/group/api';
import { getOrgList } from '@/web/support/user/team/org/api';
import { useScrollPagination } from '@fastgpt/web/hooks/useScrollPagination';
const HoverBoxStyle = { const HoverBoxStyle = {
bgColor: 'myGray.50', bgColor: 'myGray.50',
@ -47,30 +51,27 @@ function MemberModal({
addPermissionOnly?: boolean; addPermissionOnly?: boolean;
}) { }) {
const { t } = useTranslation(); const { t } = useTranslation();
const { userInfo, loadAndGetTeamMembers, loadAndGetGroups, loadAndGetOrgs } = useUserStore(); const { userInfo } = useUserStore();
const collaboratorList = useContextSelector(CollaboratorContext, (v) => v.collaboratorList); const collaboratorList = useContextSelector(CollaboratorContext, (v) => v.collaboratorList);
const [searchText, setSearchText] = useState<string>(''); const [searchText, setSearchText] = useState<string>('');
const [filterClass, setFilterClass] = useState<'member' | 'org' | 'group'>(); const [filterClass, setFilterClass] = useState<'member' | 'org' | 'group'>();
const { data: members, ScrollData } = useScrollPagination(getTeamMembers, {
pageSize: 15
});
const { data: [members = [], groups = [], orgs = []] = [], loading: loadingMembersAndGroups } = const { data: [groups = [], orgs = []] = [], loading: loadingGroupsAndOrgs } = useRequest2(
useRequest2( async () => {
async () => { if (!userInfo?.team?.teamId) return [[], []];
if (!userInfo?.team?.teamId) return [[], []]; return Promise.all([getGroupList(), getOrgList()]);
return Promise.all([ },
loadAndGetTeamMembers(true), {
loadAndGetGroups(true), manual: false,
loadAndGetOrgs(true) refreshDeps: [userInfo?.team?.teamId]
]); }
}, );
{
manual: false,
refreshDeps: [userInfo?.team?.teamId]
}
);
const [parentPath, setParentPath] = useState(''); const [parentPath, setParentPath] = useState('');
const paths = useMemo(() => { const paths = useMemo(() => {
const splitPath = parentPath.split('/').filter(Boolean); const splitPath = parentPath.split('/').filter(Boolean);
return splitPath return splitPath
@ -212,7 +213,7 @@ function MemberModal({
h={'100%'} h={'100%'}
maxH={'90vh'} maxH={'90vh'}
isCentered isCentered
isLoading={loadingMembersAndGroups} isLoading={loadingGroupsAndOrgs}
> >
<ModalBody flex={'1'}> <ModalBody flex={'1'}>
<Grid <Grid
@ -236,6 +237,7 @@ function MemberModal({
/> />
<Flex flexDirection="column" mt="3" overflow={'auto'} flex={'1 0 0'} h={0}> <Flex flexDirection="column" mt="3" overflow={'auto'} flex={'1 0 0'} h={0}>
{/* Entry */}
{!searchText && !filterClass && ( {!searchText && !filterClass && (
<> <>
{entryList.current.map((item) => { {entryList.current.map((item) => {
@ -298,142 +300,135 @@ function MemberModal({
</Box> </Box>
)} )}
<Flex flexDirection={'column'} gap={1} userSelect={'none'}> {filterClass && (
{filterMembers.map((member) => { <ScrollData
const collaborator = collaboratorList?.find((v) => v.tmbId === member.tmbId); flexDirection={'column'}
const disabled = addOnly && collaborator !== undefined; gap={1}
const onChange = () => { userSelect={'none'}
if (disabled) return; height={'fit-content'}
setSelectedMembers((state) => { >
if (state.includes(member.tmbId)) { {filterOrgs.map((org) => {
return state.filter((v) => v !== member.tmbId); const onChange = () => {
} setSelectedOrgIdList((state) => {
return [...state, member.tmbId]; if (state.includes(org._id)) {
}); return state.filter((v) => v !== org._id);
}; }
return ( return [...state, org._id];
<HStack });
justifyContent="space-between" };
key={member.tmbId} const collaborator = collaboratorList?.find((v) => v.orgId === org._id);
py="2" return (
px="3" <HStack
borderRadius="sm" justifyContent="space-between"
alignItems="center" key={org._id}
_hover={HoverBoxStyle} py="2"
onClick={onChange} px="3"
> borderRadius="sm"
<Checkbox alignItems="center"
isDisabled={disabled} _hover={HoverBoxStyle}
isChecked={disabled || selectedMemberIdList.includes(member.tmbId)} onClick={onChange}
pointerEvents="none" >
/> <Checkbox
<MyAvatar src={member.avatar} w="1.5rem" borderRadius={'50%'} /> isChecked={selectedOrgIdList.includes(org._id)}
<Box w="full" ml="2"> pointerEvents="none"
{member.memberName} />
</Box> <MyAvatar src={org.avatar} w="1.5rem" borderRadius={'50%'} />
<PermissionTags <HStack ml="2" w="full" gap="5px">
permission={addOnly ? undefined : collaborator?.permission.value} <Text>{org.name}</Text>
/> {org.count && (
</HStack> <>
); <Tag size="sm" my="auto">
})} {org.count}
{filterOrgs.map((org) => { </Tag>
const collaborator = collaboratorList?.find((v) => v.orgId === org._id); </>
const disabled = addOnly && collaborator !== undefined; )}
const onChange = () => { </HStack>
if (disabled) return; <PermissionTags permission={collaborator?.permission.value} />
setSelectedOrgIdList((state) => {
if (state.includes(org._id)) {
return state.filter((v) => v !== org._id);
}
return [...state, org._id];
});
};
return (
<HStack
justifyContent="space-between"
key={org._id}
py="2"
px="3"
borderRadius="sm"
alignItems="center"
_hover={HoverBoxStyle}
onClick={onChange}
>
<Checkbox
isDisabled={disabled}
isChecked={disabled || selectedOrgIdList.includes(org._id)}
pointerEvents="none"
/>
<MyAvatar src={org.avatar} w="1.5rem" borderRadius={'50%'} />
<HStack ml="2" w="full" gap="5px">
<Text>{org.name}</Text>
{org.count && ( {org.count && (
<Tag size="sm" my="auto"> <MyIcon
{org.count} name="core/chat/chevronRight"
</Tag> w="16px"
p="4px"
rounded={'6px'}
_hover={{
bgColor: 'myGray.200'
}}
onClick={() => {
setParentPath(getOrgChildrenPath(org));
}}
/>
)} )}
</HStack> </HStack>
<PermissionTags );
permission={addOnly ? undefined : collaborator?.permission.value} })}
/> {filterMembers.map((member) => {
{org.count && ( const onChange = () => {
<MyIcon setSelectedMembers((state) => {
name="core/chat/chevronRight" if (state.includes(member.tmbId)) {
w="16px" return state.filter((v) => v !== member.tmbId);
p="4px" }
rounded={'6px'} return [...state, member.tmbId];
_hover={{ });
bgColor: 'myGray.200' };
}} const collaborator = collaboratorList?.find((v) => v.tmbId === member.tmbId);
onClick={(e) => { return (
e.stopPropagation(); <HStack
setParentPath(getOrgChildrenPath(org)); justifyContent="space-between"
}} key={member.tmbId}
py="2"
px="3"
borderRadius="sm"
alignItems="center"
_hover={HoverBoxStyle}
onClick={onChange}
>
<Checkbox
isChecked={selectedMemberIdList.includes(member.tmbId)}
pointerEvents="none"
/> />
)} <MyAvatar src={member.avatar} w="1.5rem" borderRadius={'50%'} />
</HStack> <Box w="full" ml="2">
); {member.memberName}
})} </Box>
{filterGroups.map((group) => { <PermissionTags permission={collaborator?.permission.value} />
const collaborator = collaboratorList?.find((v) => v.groupId === group._id); </HStack>
const disabled = addOnly && collaborator !== undefined; );
const onChange = () => { })}
if (disabled) return; {filterGroups.map((group) => {
setSelectedGroupIdList((state) => { const onChange = () => {
if (state.includes(group._id)) { setSelectedGroupIdList((state) => {
return state.filter((v) => v !== group._id); if (state.includes(group._id)) {
} return state.filter((v) => v !== group._id);
return [...state, group._id]; }
}); return [...state, group._id];
}; });
return ( };
<HStack const collaborator = collaboratorList?.find((v) => v.groupId === group._id);
justifyContent="space-between" return (
key={group._id} <HStack
py="2" justifyContent="space-between"
px="3" key={group._id}
borderRadius="sm" py="2"
alignItems="center" px="3"
_hover={HoverBoxStyle} borderRadius="sm"
onClick={onChange} alignItems="center"
> _hover={HoverBoxStyle}
<Checkbox onClick={onChange}
isDisabled={disabled} >
isChecked={disabled || selectedGroupIdList.includes(group._id)} <Checkbox
pointerEvents="none" isChecked={selectedGroupIdList.includes(group._id)}
/> pointerEvents="none"
<MyAvatar src={group.avatar} w="1.5rem" borderRadius={'50%'} /> />
<Box ml="2" w="full"> <MyAvatar src={group.avatar} w="1.5rem" borderRadius={'50%'} />
{group.name === DefaultGroupName ? userInfo?.team.teamName : group.name} <Box ml="2" w="full">
</Box> {group.name === DefaultGroupName ? userInfo?.team.teamName : group.name}
<PermissionTags </Box>
permission={addOnly ? undefined : collaborator?.permission.value} <PermissionTags permission={collaborator?.permission.value} />
/> </HStack>
</HStack> );
); })}
})} </ScrollData>
</Flex> )}
</Flex> </Flex>
</Flex> </Flex>

View File

@ -1,7 +1,7 @@
import { RequestPaging } from '@/types'; import { PaginationProps } from '@fastgpt/web/common/fetch/type';
export type GetAppChatLogsParams = RequestPaging & { export type GetAppChatLogsParams = PaginationProps<{
appId: string; appId: string;
dateStart: Date; dateStart: Date;
dateEnd: Date; dateEnd: Date;
}; }>;

View File

@ -3,24 +3,24 @@ import {
DatasetCollectionTypeEnum, DatasetCollectionTypeEnum,
DatasetTypeEnum DatasetTypeEnum
} from '@fastgpt/global/core/dataset/constants'; } from '@fastgpt/global/core/dataset/constants';
import type { RequestPaging } from '@/types';
import { TrainingModeEnum } from '@fastgpt/global/core/dataset/constants'; import { TrainingModeEnum } from '@fastgpt/global/core/dataset/constants';
import type { SearchTestItemType } from '@/types/core/dataset'; import type { SearchTestItemType } from '@/types/core/dataset';
import { UploadChunkItemType } from '@fastgpt/global/core/dataset/type'; import { UploadChunkItemType } from '@fastgpt/global/core/dataset/type';
import { DatasetCollectionSchemaType } from '@fastgpt/global/core/dataset/type'; import { DatasetCollectionSchemaType } from '@fastgpt/global/core/dataset/type';
import { PermissionTypeEnum } from '@fastgpt/global/support/permission/constant'; import { PermissionTypeEnum } from '@fastgpt/global/support/permission/constant';
import type { LLMModelItemType } from '@fastgpt/global/core/ai/model.d'; import type { LLMModelItemType } from '@fastgpt/global/core/ai/model.d';
import { PaginationProps } from '@fastgpt/web/common/fetch/type';
/* ===== dataset ===== */ /* ===== dataset ===== */
/* ======= collections =========== */ /* ======= collections =========== */
export type GetDatasetCollectionsProps = RequestPaging & { export type GetDatasetCollectionsProps = PaginationProps<{
datasetId: string; datasetId: string;
parentId?: string; parentId?: string;
searchText?: string; searchText?: string;
filterTags?: string[]; filterTags?: string[];
simple?: boolean; simple?: boolean;
selectFolder?: boolean; selectFolder?: boolean;
}; }>;
/* ==== data ===== */ /* ==== data ===== */

View File

@ -55,7 +55,7 @@ function GroupInfoModal({ onClose, editGroupId }: { onClose: () => void; editGro
} }
); );
const { run: onCreate, loading: isLoadingCreate } = useRequest2( const { runAsync: onCreate, loading: isLoadingCreate } = useRequest2(
(data: GroupFormType) => { (data: GroupFormType) => {
return postCreateGroup({ return postCreateGroup({
name: data.name, name: data.name,
@ -67,7 +67,7 @@ function GroupInfoModal({ onClose, editGroupId }: { onClose: () => void; editGro
} }
); );
const { run: onUpdate, loading: isLoadingUpdate } = useRequest2( const { runAsync: onUpdate, loading: isLoadingUpdate } = useRequest2(
async (data: GroupFormType) => { async (data: GroupFormType) => {
if (!editGroupId) return; if (!editGroupId) return;
return putUpdateGroup({ return putUpdateGroup({

View File

@ -32,26 +32,26 @@ export type GroupFormType = {
}[]; }[];
}; };
// 1. Owner can not be deleted, toast
// 2. Owner/Admin can manage members
// 3. Owner can add/remove admins
function GroupEditModal({ onClose, editGroupId }: { onClose: () => void; editGroupId?: string }) { function GroupEditModal({ onClose, editGroupId }: { onClose: () => void; editGroupId?: string }) {
// 1. Owner can not be deleted, toast
// 2. Owner/Admin can manage members
// 3. Owner can add/remove admins
const { t } = useTranslation(); const { t } = useTranslation();
const { userInfo } = useUserStore(); const { userInfo } = useUserStore();
const { toast } = useToast(); const { toast } = useToast();
const [hoveredMemberId, setHoveredMemberId] = useState<string | undefined>(undefined);
const {
members: allMembers,
refetchGroups,
groups,
refetchMembers
} = useContextSelector(TeamContext, (v) => v);
const groups = useContextSelector(TeamContext, (v) => v.groups);
const refetchGroups = useContextSelector(TeamContext, (v) => v.refetchGroups);
const group = useMemo(() => { const group = useMemo(() => {
return groups.find((item) => item._id === editGroupId); return groups.find((item) => item._id === editGroupId);
}, [editGroupId, groups]); }, [editGroupId, groups]);
const allMembers = useContextSelector(TeamContext, (v) => v.members);
const refetchMembers = useContextSelector(TeamContext, (v) => v.refetchMembers);
const MemberScrollData = useContextSelector(TeamContext, (v) => v.MemberScrollData);
const [hoveredMemberId, setHoveredMemberId] = useState<string>();
const [members, setMembers] = useState(group?.members || []); const [members, setMembers] = useState(group?.members || []);
const [searchKey, setSearchKey] = useState(''); const [searchKey, setSearchKey] = useState('');
const filtered = useMemo(() => { const filtered = useMemo(() => {
return [ return [
@ -62,7 +62,7 @@ function GroupEditModal({ onClose, editGroupId }: { onClose: () => void; editGro
]; ];
}, [searchKey, allMembers]); }, [searchKey, allMembers]);
const { run: onUpdate, loading: isLoadingUpdate } = useRequest2( const { runAsync: onUpdate, loading: isLoadingUpdate } = useRequest2(
async () => { async () => {
if (!editGroupId || !members.length) return; if (!editGroupId || !members.length) return;
return putUpdateGroup({ return putUpdateGroup({
@ -155,7 +155,7 @@ function GroupEditModal({ onClose, editGroupId }: { onClose: () => void; editGro
setSearchKey(e.target.value); setSearchKey(e.target.value);
}} }}
/> />
<Flex flexDirection="column" mt={3} flexGrow="1" overflow={'auto'} maxH={'400px'}> <MemberScrollData mt={3} flex={'1 0 0'} h={0}>
{filtered.map((member) => { {filtered.map((member) => {
return ( return (
<HStack <HStack
@ -181,11 +181,11 @@ function GroupEditModal({ onClose, editGroupId }: { onClose: () => void; editGro
</HStack> </HStack>
); );
})} })}
</Flex> </MemberScrollData>
</Flex> </Flex>
<Flex borderLeft="1px" borderColor="myGray.200" flexDirection="column" p="4" h={'100%'}> <Flex borderLeft="1px" borderColor="myGray.200" flexDirection="column" p="4" h={'100%'}>
<Box mt={2}>{t('common:chosen') + ': ' + members.length}</Box> <Box mt={2}>{t('common:chosen') + ': ' + members.length}</Box>
<Flex mt={3} flexDirection="column" flexGrow="1" overflow={'auto'} maxH={'400px'}> <MemberScrollData mt={3} flex={'1 0 0'} h={0}>
{members.map((member) => { {members.map((member) => {
return ( return (
<HStack <HStack
@ -262,11 +262,14 @@ function GroupEditModal({ onClose, editGroupId }: { onClose: () => void; editGro
</HStack> </HStack>
); );
})} })}
</Flex> </MemberScrollData>
</Flex> </Flex>
</Grid> </Grid>
</ModalBody> </ModalBody>
<ModalFooter alignItems="flex-end"> <ModalFooter>
<Button variant={'whiteBase'} mr={3} onClick={onClose}>
{t('common:common.Close')}
</Button>
<Button isLoading={isLoading} onClick={onUpdate}> <Button isLoading={isLoading} onClick={onUpdate}>
{t('common:common.Save')} {t('common:common.Save')}
</Button> </Button>

View File

@ -24,50 +24,43 @@ import { useUserStore } from '@/web/support/user/useUserStore';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest'; import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { deleteGroup } from '@/web/support/user/team/group/api'; import { deleteGroup } from '@/web/support/user/team/group/api';
import { DefaultGroupName } from '@fastgpt/global/support/user/team/group/constant'; import { DefaultGroupName } from '@fastgpt/global/support/user/team/group/constant';
import MemberTag from '../../../../../components/support/user/team/Info/MemberTag'; import MemberTag from '../../../../components/support/user/team/Info/MemberTag';
import MyTooltip from '@fastgpt/web/components/common/MyTooltip'; import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
import dynamic from 'next/dynamic'; import dynamic from 'next/dynamic';
import { useState } from 'react'; import { useState } from 'react';
import IconButton from '../OrgManage/IconButton'; import IconButton from '../OrgManage/IconButton';
import { MemberGroupType } from '@fastgpt/global/support/permission/memberGroup/type';
const ChangeOwnerModal = dynamic(() => import('./GroupTransferOwnerModal')); const ChangeOwnerModal = dynamic(() => import('./GroupTransferOwnerModal'));
const GroupInfoModal = dynamic(() => import('./GroupInfoModal')); const GroupInfoModal = dynamic(() => import('./GroupInfoModal'));
const ManageGroupMemberModal = dynamic(() => import('./GroupManageMember')); const GroupManageMember = dynamic(() => import('./GroupManageMember'));
function MemberTable({ Tabs }: { Tabs: React.ReactNode }) { function MemberTable({ Tabs }: { Tabs: React.ReactNode }) {
const { t } = useTranslation(); const { t } = useTranslation();
const { userInfo } = useUserStore(); const { userInfo } = useUserStore();
const [editGroupId, setEditGroupId] = useState<string>(); const { groups, refetchGroups, members, refetchMembers } = useContextSelector(
TeamContext,
(v) => v
);
const [editGroup, setEditGroup] = useState<MemberGroupType>();
const { const {
isOpen: isOpenGroupInfo, isOpen: isOpenGroupInfo,
onOpen: onOpenGroupInfo, onOpen: onOpenGroupInfo,
onClose: onCloseGroupInfo onClose: onCloseGroupInfo
} = useDisclosure(); } = useDisclosure();
const {
isOpen: isOpenManageGroupMember, const onEditGroupInfo = (e: MemberGroupType) => {
onOpen: onOpenManageGroupMember, setEditGroup(e);
onClose: onCloseManageGroupMember
} = useDisclosure();
const onEditGroup = (groupId: string) => {
setEditGroupId(groupId);
onOpenGroupInfo(); onOpenGroupInfo();
}; };
const onManageMember = (groupId: string) => {
setEditGroupId(groupId);
onOpenManageGroupMember();
};
const { ConfirmModal: ConfirmDeleteGroupModal, openConfirm: openDeleteGroupModal } = useConfirm({ const { ConfirmModal: ConfirmDeleteGroupModal, openConfirm: openDeleteGroupModal } = useConfirm({
type: 'delete', type: 'delete',
content: t('account_team:confirm_delete_group') content: t('account_team:confirm_delete_group')
}); });
const { groups, refetchGroups, members, refetchMembers } = useContextSelector(
TeamContext,
(v) => v
);
const { runAsync: delDeleteGroup } = useRequest2(deleteGroup, { const { runAsync: delDeleteGroup } = useRequest2(deleteGroup, {
onSuccess: () => { onSuccess: () => {
refetchGroups(); refetchGroups();
@ -75,12 +68,21 @@ function MemberTable({ Tabs }: { Tabs: React.ReactNode }) {
} }
}); });
const {
isOpen: isOpenManageGroupMember,
onOpen: onOpenManageGroupMember,
onClose: onCloseManageGroupMember
} = useDisclosure();
const onManageMember = (e: MemberGroupType) => {
setEditGroup(e);
onOpenManageGroupMember();
};
const hasGroupManagePer = (group: (typeof groups)[0]) => const hasGroupManagePer = (group: (typeof groups)[0]) =>
userInfo?.team.permission.hasManagePer || userInfo?.team.permission.hasManagePer ||
['admin', 'owner'].includes( ['admin', 'owner'].includes(
group.members.find((item) => item.tmbId === userInfo?.team.tmbId)?.role ?? '' group.members.find((item) => item.tmbId === userInfo?.team.tmbId)?.role ?? ''
); );
const isGroupOwner = (group: (typeof groups)[0]) => const isGroupOwner = (group: (typeof groups)[0]) =>
userInfo?.team.permission.hasManagePer || userInfo?.team.permission.hasManagePer ||
group.members.find((item) => item.role === 'owner')?.tmbId === userInfo?.team.tmbId; group.members.find((item) => item.role === 'owner')?.tmbId === userInfo?.team.tmbId;
@ -90,8 +92,8 @@ function MemberTable({ Tabs }: { Tabs: React.ReactNode }) {
onOpen: onOpenChangeOwner, onOpen: onOpenChangeOwner,
onClose: onCloseChangeOwner onClose: onCloseChangeOwner
} = useDisclosure(); } = useDisclosure();
const onChangeOwner = (groupId: string) => { const onChangeOwner = (e: MemberGroupType) => {
setEditGroupId(groupId); setEditGroup(e);
onOpenChangeOwner(); onOpenChangeOwner();
}; };
@ -173,7 +175,7 @@ function MemberTable({ Tabs }: { Tabs: React.ReactNode }) {
<AvatarGroup avatars={members.map((v) => v.avatar)} groupId={group._id} /> <AvatarGroup avatars={members.map((v) => v.avatar)} groupId={group._id} />
) : hasGroupManagePer(group) ? ( ) : hasGroupManagePer(group) ? (
<MyTooltip label={t('account_team:manage_member')}> <MyTooltip label={t('account_team:manage_member')}>
<Box cursor="pointer" onClick={() => onManageMember(group._id)}> <Box cursor="pointer" onClick={() => onManageMember(group)}>
<AvatarGroup <AvatarGroup
avatars={group.members.map( avatars={group.members.map(
(v) => members.find((m) => m.tmbId === v.tmbId)?.avatar ?? '' (v) => members.find((m) => m.tmbId === v.tmbId)?.avatar ?? ''
@ -202,14 +204,14 @@ function MemberTable({ Tabs }: { Tabs: React.ReactNode }) {
label: t('account_team:edit_info'), label: t('account_team:edit_info'),
icon: 'edit', icon: 'edit',
onClick: () => { onClick: () => {
onEditGroup(group._id); onEditGroupInfo(group);
} }
}, },
{ {
label: t('account_team:manage_member'), label: t('account_team:manage_member'),
icon: 'support/team/group', icon: 'support/team/group',
onClick: () => { onClick: () => {
onManageMember(group._id); onManageMember(group);
} }
}, },
...(isGroupOwner(group) ...(isGroupOwner(group)
@ -218,7 +220,7 @@ function MemberTable({ Tabs }: { Tabs: React.ReactNode }) {
label: t('account_team:transfer_ownership'), label: t('account_team:transfer_ownership'),
icon: 'modal/changePer', icon: 'modal/changePer',
onClick: () => { onClick: () => {
onChangeOwner(group._id); onChangeOwner(group);
}, },
type: 'primary' as MenuItemType type: 'primary' as MenuItemType
}, },
@ -246,25 +248,25 @@ function MemberTable({ Tabs }: { Tabs: React.ReactNode }) {
</MyBox> </MyBox>
<ConfirmDeleteGroupModal /> <ConfirmDeleteGroupModal />
{isOpenChangeOwner && editGroupId && ( {isOpenChangeOwner && editGroup && (
<ChangeOwnerModal groupId={editGroupId} onClose={onCloseChangeOwner} /> <ChangeOwnerModal groupId={editGroup._id} onClose={onCloseChangeOwner} />
)} )}
{isOpenGroupInfo && ( {isOpenGroupInfo && (
<GroupInfoModal <GroupInfoModal
onClose={() => { onClose={() => {
onCloseGroupInfo(); onCloseGroupInfo();
setEditGroupId(undefined); setEditGroup(undefined);
}} }}
editGroupId={editGroupId} editGroupId={editGroup?._id}
/> />
)} )}
{isOpenManageGroupMember && ( {isOpenManageGroupMember && editGroup && (
<ManageGroupMemberModal <GroupManageMember
onClose={() => { onClose={() => {
onCloseManageGroupMember(); onCloseManageGroupMember();
setEditGroupId(undefined); setEditGroup(undefined);
}} }}
editGroupId={editGroupId} editGroupId={editGroup._id}
/> />
)} )}
</> </>

View File

@ -13,7 +13,6 @@ import {
Tr, Tr,
useDisclosure useDisclosure
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import { TeamMemberRoleEnum } from '@fastgpt/global/support/user/team/constant';
import { useTranslation } from 'next-i18next'; import { useTranslation } from 'next-i18next';
import { useUserStore } from '@/web/support/user/useUserStore'; import { useUserStore } from '@/web/support/user/useUserStore';
import { useConfirm } from '@fastgpt/web/hooks/useConfirm'; import { useConfirm } from '@fastgpt/web/hooks/useConfirm';
@ -30,6 +29,9 @@ import { useToast } from '@fastgpt/web/hooks/useToast';
import { TeamErrEnum } from '@fastgpt/global/common/error/code/team'; import { TeamErrEnum } from '@fastgpt/global/common/error/code/team';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest'; import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { delLeaveTeam } from '@/web/support/user/team/api'; import { delLeaveTeam } from '@/web/support/user/team/api';
import { postSyncMembers } from '@/web/support/user/api';
import MyLoading from '@fastgpt/web/components/common/MyLoading';
import { TeamMemberRoleEnum } from '@fastgpt/global/support/user/team/constant';
const InviteModal = dynamic(() => import('./InviteModal')); const InviteModal = dynamic(() => import('./InviteModal'));
const TeamTagModal = dynamic(() => import('@/components/support/user/team/TeamTagModal')); const TeamTagModal = dynamic(() => import('@/components/support/user/team/TeamTagModal'));
@ -40,8 +42,16 @@ function MemberTable({ Tabs }: { Tabs: React.ReactNode }) {
const { userInfo, teamPlanStatus } = useUserStore(); const { userInfo, teamPlanStatus } = useUserStore();
const { feConfigs, setNotSufficientModalType } = useSystemStore(); const { feConfigs, setNotSufficientModalType } = useSystemStore();
const { groups, refetchGroups, myTeams, refetchTeams, members, refetchMembers, onSwitchTeam } = const {
useContextSelector(TeamContext, (v) => v); groups,
refetchGroups,
myTeams,
refetchTeams,
members,
refetchMembers,
onSwitchTeam,
MemberScrollData
} = useContextSelector(TeamContext, (v) => v);
const { const {
isOpen: isOpenTeamTagsAsync, isOpen: isOpenTeamTagsAsync,
@ -54,6 +64,8 @@ function MemberTable({ Tabs }: { Tabs: React.ReactNode }) {
type: 'delete' type: 'delete'
}); });
const isSyncMember = feConfigs.register_method?.includes('sync');
const { runAsync: onLeaveTeam } = useRequest2( const { runAsync: onLeaveTeam } = useRequest2(
async () => { async () => {
const defaultTeam = myTeams.find((item) => item.defaultTeam) || myTeams[0]; const defaultTeam = myTeams.find((item) => item.defaultTeam) || myTeams[0];
@ -72,8 +84,17 @@ function MemberTable({ Tabs }: { Tabs: React.ReactNode }) {
content: t('account_team:confirm_leave_team') content: t('account_team:confirm_leave_team')
}); });
const { runAsync: onSyncMember, loading: isSyncing } = useRequest2(postSyncMembers, {
onSuccess() {
refetchMembers();
},
successToast: t('account_team:sync_member_success'),
errorToast: t('account_team:sync_member_failed')
});
return ( return (
<> <>
{isSyncing && <MyLoading />}
<Flex justify={'space-between'} align={'center'} pb={'1rem'}> <Flex justify={'space-between'} align={'center'} pb={'1rem'}>
{Tabs} {Tabs}
<HStack> <HStack>
@ -91,7 +112,21 @@ function MemberTable({ Tabs }: { Tabs: React.ReactNode }) {
{t('account_team:label_sync')} {t('account_team:label_sync')}
</Button> </Button>
)} )}
{userInfo?.team.permission.hasManagePer && ( {userInfo?.team.permission.hasManagePer && isSyncMember && (
<Button
variant={'primary'}
size="md"
borderRadius={'md'}
ml={3}
leftIcon={<MyIcon name="common/retryLight" w={'16px'} color={'white'} />}
onClick={() => {
onSyncMember();
}}
>
{t('account_team:sync_immediately')}
</Button>
)}
{userInfo?.team.permission.hasManagePer && !isSyncMember && (
<Button <Button
variant={'primary'} variant={'primary'}
size="md" size="md"
@ -135,76 +170,83 @@ function MemberTable({ Tabs }: { Tabs: React.ReactNode }) {
<Box flex={'1 0 0'} overflow={'auto'}> <Box flex={'1 0 0'} overflow={'auto'}>
<TableContainer overflow={'unset'} fontSize={'sm'}> <TableContainer overflow={'unset'} fontSize={'sm'}>
<Table overflow={'unset'}> <MemberScrollData>
<Thead> <Table overflow={'unset'}>
<Tr bgColor={'white !important'}> <Thead>
<Th borderLeftRadius="6px" bgColor="myGray.100"> <Tr bgColor={'white !important'}>
{t('account_team:user_name')} <Th borderLeftRadius="6px" bgColor="myGray.100">
</Th> {t('account_team:user_name')}
<Th bgColor="myGray.100">{t('account_team:member_group')}</Th> </Th>
<Th borderRightRadius="6px" bgColor="myGray.100"> <Th bgColor="myGray.100">{t('account_team:member_group')}</Th>
{t('common:common.Action')} {!isSyncMember && (
</Th> <Th borderRightRadius="6px" bgColor="myGray.100">
</Tr> {t('common:common.Action')}
</Thead> </Th>
<Tbody> )}
{members?.map((item) => (
<Tr key={item.userId} overflow={'unset'}>
<Td>
<HStack>
<Avatar src={item.avatar} w={['18px', '22px']} borderRadius={'50%'} />
<Box className={'textEllipsis'}>
{item.memberName}
{item.status === 'waiting' && (
<Tag ml="2" colorSchema="yellow">
{t('account_team:waiting')}
</Tag>
)}
</Box>
</HStack>
</Td>
<Td maxW={'300px'}>
<GroupTags
names={groups
?.filter((group) => group.members.map((m) => m.tmbId).includes(item.tmbId))
.map((g) => g.name)}
max={3}
/>
</Td>
<Td>
{userInfo?.team.permission.hasManagePer &&
item.role !== TeamMemberRoleEnum.owner &&
item.tmbId !== userInfo?.team.tmbId && (
<Icon
name={'common/trash'}
cursor={'pointer'}
w="1rem"
p="1"
borderRadius="sm"
_hover={{
color: 'red.600',
bgColor: 'myGray.100'
}}
onClick={() => {
openRemoveMember(
() =>
delRemoveMember(item.tmbId).then(() =>
Promise.all([refetchGroups(), refetchMembers()])
),
undefined,
t('account_team:remove_tip', {
username: item.memberName
})
)();
}}
/>
)}
</Td>
</Tr> </Tr>
))} </Thead>
</Tbody> <Tbody>
</Table> {members?.map((item) => (
<Tr key={item.userId} overflow={'unset'}>
<Td>
<HStack>
<Avatar src={item.avatar} w={['18px', '22px']} borderRadius={'50%'} />
<Box className={'textEllipsis'}>
{item.memberName}
{item.status === 'waiting' && (
<Tag ml="2" colorSchema="yellow">
{t('account_team:waiting')}
</Tag>
)}
</Box>
</HStack>
</Td>
<Td maxW={'300px'}>
<GroupTags
names={groups
?.filter((group) =>
group.members.map((m) => m.tmbId).includes(item.tmbId)
)
.map((g) => g.name)}
max={3}
/>
</Td>
{!isSyncMember && (
<Td>
{userInfo?.team.permission.hasManagePer &&
item.role !== TeamMemberRoleEnum.owner &&
item.tmbId !== userInfo?.team.tmbId && (
<Icon
name={'common/trash'}
cursor={'pointer'}
w="1rem"
p="1"
borderRadius="sm"
_hover={{
color: 'red.600',
bgColor: 'myGray.100'
}}
onClick={() => {
openRemoveMember(
() =>
delRemoveMember(item.tmbId).then(() =>
Promise.all([refetchGroups(), refetchMembers()])
),
undefined,
t('account_team:remove_tip', {
username: item.memberName
})
)();
}}
/>
)}
</Td>
)}
</Tr>
))}
</Tbody>
</Table>
</MemberScrollData>
<ConfirmRemoveMemberModal /> <ConfirmRemoveMemberModal />
</TableContainer> </TableContainer>
</Box> </Box>

View File

@ -22,7 +22,6 @@ import { useMemo, useState } from 'react';
import { useContextSelector } from 'use-context-selector'; import { useContextSelector } from 'use-context-selector';
import { TeamContext } from '../context'; import { TeamContext } from '../context';
import { OrgType } from '@fastgpt/global/support/user/team/org/type'; import { OrgType } from '@fastgpt/global/support/user/team/org/type';
import dynamic from 'next/dynamic';
export type GroupFormType = { export type GroupFormType = {
members: { members: {
@ -51,7 +50,7 @@ function OrgMemberManageModal({
onClose: () => void; onClose: () => void;
}) { }) {
const { t } = useTranslation(); const { t } = useTranslation();
const allMembers = useContextSelector(TeamContext, (v) => v.members); const { members: allMembers, MemberScrollData } = useContextSelector(TeamContext, (v) => v);
const [selectedMembers, setSelectedMembers] = useState<string[]>( const [selectedMembers, setSelectedMembers] = useState<string[]>(
currentOrg.members.map((item) => item.tmbId) currentOrg.members.map((item) => item.tmbId)
@ -98,7 +97,6 @@ function OrgMemberManageModal({
return ( return (
<MyModal <MyModal
onClose={onClose}
isOpen isOpen
title={t('user:team.group.manage_member')} title={t('user:team.group.manage_member')}
iconSrc={currentOrg?.avatar} iconSrc={currentOrg?.avatar}
@ -124,32 +122,34 @@ function OrgMemberManageModal({
}} }}
/> />
<Flex flexDirection="column" mt={3} flexGrow="1" overflow={'auto'} maxH={'400px'}> <Flex flexDirection="column" mt={3} flexGrow="1" overflow={'auto'} maxH={'400px'}>
{filterMembers.map((member) => { <MemberScrollData>
return ( {filterMembers.map((member) => {
<HStack return (
py="2" <HStack
px={3} py="2"
borderRadius={'md'} px={3}
alignItems="center" borderRadius={'md'}
key={member.tmbId} alignItems="center"
cursor={'pointer'} key={member.tmbId}
_hover={{ cursor={'pointer'}
bg: 'myGray.50', _hover={{
...(!isSelected(member.tmbId) ? { svg: { color: 'myGray.50' } } : {}) bg: 'myGray.50',
}} ...(!isSelected(member.tmbId) ? { svg: { color: 'myGray.50' } } : {})
_notLast={{ mb: 2 }} }}
onClick={() => handleToggleSelect(member.tmbId)} _notLast={{ mb: 2 }}
> onClick={() => handleToggleSelect(member.tmbId)}
<Checkbox >
isChecked={!!isSelected(member.tmbId)} <Checkbox
icon={<CheckboxIcon name={'common/check'} />} isChecked={!!isSelected(member.tmbId)}
pointerEvents="none" icon={<CheckboxIcon name={'common/check'} />}
/> pointerEvents="none"
<Avatar src={member.avatar} w="1.5rem" borderRadius={'50%'} /> />
<Box>{member.memberName}</Box> <Avatar src={member.avatar} w="1.5rem" borderRadius={'50%'} />
</HStack> <Box>{member.memberName}</Box>
); </HStack>
})} );
})}
</MemberScrollData>
</Flex> </Flex>
</Flex> </Flex>
<Flex borderLeft="1px" borderColor="myGray.200" flexDirection="column" p="4" h={'100%'}> <Flex borderLeft="1px" borderColor="myGray.200" flexDirection="column" p="4" h={'100%'}>
@ -185,7 +185,10 @@ function OrgMemberManageModal({
</Flex> </Flex>
</Grid> </Grid>
</ModalBody> </ModalBody>
<ModalFooter alignItems="flex-end"> <ModalFooter>
<Button variant={'whiteBase'} mr={3} onClick={onClose}>
{t('common:common.Close')}
</Button>
<Button isLoading={isLoading} onClick={onUpdate}> <Button isLoading={isLoading} onClick={onUpdate}>
{t('common:common.Save')} {t('common:common.Save')}
</Button> </Button>

View File

@ -0,0 +1,369 @@
import { useUserStore } from '@/web/support/user/useUserStore';
import {
Box,
Divider,
Flex,
HStack,
Table,
TableContainer,
Tag,
Tbody,
Td,
Th,
Thead,
Tr,
VStack
} from '@chakra-ui/react';
import type { OrgType } from '@fastgpt/global/support/user/team/org/type';
import Avatar from '@fastgpt/web/components/common/Avatar';
import MyIcon from '@fastgpt/web/components/common/Icon';
import type { IconNameType } from '@fastgpt/web/components/common/Icon/type';
import MyMenu from '@fastgpt/web/components/common/MyMenu';
import { useConfirm } from '@fastgpt/web/hooks/useConfirm';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { useTranslation } from 'next-i18next';
import { useMemo, useState } from 'react';
import { useContextSelector } from 'use-context-selector';
import MemberTag from '@/components/support/user/team/Info/MemberTag';
import { TeamContext } from '../context';
import { deleteOrg, deleteOrgMember, getOrgList } from '@/web/support/user/team/org/api';
import IconButton from './IconButton';
import { defaultOrgForm, type OrgFormType } from './OrgInfoModal';
import dynamic from 'next/dynamic';
import MyBox from '@fastgpt/web/components/common/MyBox';
import Path from '@/components/common/folder/Path';
import { ParentTreePathItemType } from '@fastgpt/global/common/parentFolder/type';
import { getOrgChildrenPath } from '@fastgpt/global/support/user/team/org/constant';
import { useSystemStore } from '@/web/common/system/useSystemStore';
const OrgInfoModal = dynamic(() => import('./OrgInfoModal'));
const OrgMemberManageModal = dynamic(() => import('./OrgMemberManageModal'));
const OrgMoveModal = dynamic(() => import('./OrgMoveModal'));
function ActionButton({
icon,
text,
onClick
}: {
icon: IconNameType;
text: string;
onClick: () => void;
}) {
return (
<HStack
gap={'8px'}
w="100%"
transition={'background 0.1s'}
cursor={'pointer'}
p="4px"
rounded={'sm'}
_hover={{
bg: 'myGray.05',
color: 'primary.600'
}}
onClick={onClick}
>
<MyIcon name={icon} w="1rem" h="1rem" />
<Box fontSize={'sm'}>{text}</Box>
</HStack>
);
}
function OrgTable({ Tabs }: { Tabs: React.ReactNode }) {
const { t } = useTranslation();
const { userInfo, isTeamAdmin } = useUserStore();
const { members, MemberScrollData } = useContextSelector(TeamContext, (v) => v);
const { feConfigs } = useSystemStore();
const isSyncMember = feConfigs.register_method?.includes('sync');
const [parentPath, setParentPath] = useState('');
const {
data: orgs = [],
loading: isLoadingOrgs,
refresh: refetchOrgs
} = useRequest2(getOrgList, {
manual: false,
refreshDeps: [userInfo?.team?.teamId]
});
const currentOrgs = useMemo(() => {
if (orgs.length === 0) return [];
// Auto select the first org(root org is team)
if (parentPath === '') {
setParentPath(getOrgChildrenPath(orgs[0]));
return [];
}
return orgs
.filter((org) => org.path === parentPath)
.map((item) => {
return {
...item,
// Member + org
count:
item.members.length + orgs.filter((org) => org.path === getOrgChildrenPath(item)).length
};
});
}, [orgs, parentPath]);
const currentOrg = useMemo(() => {
const splitPath = parentPath.split('/');
const currentOrgId = splitPath[splitPath.length - 1];
if (!currentOrgId) return;
return orgs.find((org) => org.pathId === currentOrgId);
}, [orgs, parentPath]);
const paths = useMemo(() => {
const splitPath = parentPath.split('/').filter(Boolean);
return splitPath
.map((id) => {
const org = orgs.find((org) => org.pathId === id)!;
if (org.path === '') return;
return {
parentId: getOrgChildrenPath(org),
parentName: org.name
};
})
.filter(Boolean) as ParentTreePathItemType[];
}, [parentPath, orgs]);
const [editOrg, setEditOrg] = useState<OrgFormType>();
const [manageMemberOrg, setManageMemberOrg] = useState<OrgType>();
const [movingOrg, setMovingOrg] = useState<OrgType>();
// Delete org
const { ConfirmModal: ConfirmDeleteOrgModal, openConfirm: openDeleteOrgModal } = useConfirm({
type: 'delete',
content: t('account_team:confirm_delete_org')
});
const deleteOrgHandler = (orgId: string) => openDeleteOrgModal(() => deleteOrgReq(orgId))();
const { runAsync: deleteOrgReq } = useRequest2(deleteOrg, {
onSuccess: () => {
refetchOrgs();
}
});
// Delete member
const { ConfirmModal: ConfirmDeleteMember, openConfirm: openDeleteMemberModal } = useConfirm({
type: 'delete',
content: t('account_team:confirm_delete_member')
});
const { runAsync: deleteMemberReq } = useRequest2(deleteOrgMember, {
onSuccess: () => {
refetchOrgs();
}
});
return (
<>
<Flex justify={'space-between'} align={'center'} pb={'1rem'}>
{Tabs}
</Flex>
<MyBox
flex={'1 0 0'}
h={0}
display={'flex'}
flexDirection={'column'}
isLoading={isLoadingOrgs}
>
<Box mb={3}>
<Path paths={paths} rootName={userInfo?.team?.teamName} onClick={setParentPath} />
</Box>
<Flex flex={'1 0 0'} h={0} w={'100%'} gap={'4'}>
<MemberScrollData h={'100%'} fontSize={'sm'} flexGrow={1}>
{/* Table */}
<TableContainer>
<Table>
<Thead>
<Tr bg={'white !important'}>
<Th bg="myGray.100" borderLeftRadius="6px">
{t('common:Name')}
</Th>
{!isSyncMember && (
<Th bg="myGray.100" borderRightRadius="6px">
{t('common:common.Action')}
</Th>
)}
</Tr>
</Thead>
<Tbody>
{currentOrgs.map((org) => (
<Tr key={org._id} overflow={'unset'}>
<Td>
<HStack
cursor={'pointer'}
onClick={() => setParentPath(getOrgChildrenPath(org))}
>
<MemberTag name={org.name} avatar={org.avatar} />
<Tag size="sm">{org.count}</Tag>
<MyIcon
name="core/chat/chevronRight"
w={'1rem'}
h={'1rem'}
color={'myGray.500'}
/>
</HStack>
</Td>
{isTeamAdmin && !isSyncMember && (
<Td w={'6rem'}>
<MyMenu
trigger="hover"
Button={<IconButton name="more" />}
menuList={[
{
children: [
{
icon: 'edit',
label: t('account_team:edit_info'),
onClick: () => setEditOrg(org)
},
{
icon: 'common/file/move',
label: t('common:Move'),
onClick: () => setMovingOrg(org)
},
{
icon: 'delete',
label: t('account_team:delete'),
type: 'danger',
onClick: () => deleteOrgHandler(org._id)
}
]
}
]}
/>
</Td>
)}
</Tr>
))}
{currentOrg?.members.map((member) => {
const memberInfo = members.find((m) => m.tmbId === member.tmbId);
if (!memberInfo) return null;
return (
<Tr key={member.tmbId}>
<Td>
<MemberTag name={memberInfo.memberName} avatar={memberInfo.avatar} />
</Td>
<Td w={'6rem'}>
{isTeamAdmin && !isSyncMember && (
<MyMenu
trigger={'hover'}
Button={<IconButton name="more" />}
menuList={[
{
children: [
{
icon: 'delete',
label: t('account_team:delete'),
type: 'danger',
onClick: () =>
openDeleteMemberModal(() =>
deleteMemberReq(currentOrg._id, member.tmbId)
)()
}
]
}
]}
/>
)}
</Td>
</Tr>
);
})}
</Tbody>
</Table>
</TableContainer>
</MemberScrollData>
{/* Slider */}
{!isSyncMember && (
<VStack w={'180px'} alignItems={'start'}>
<HStack gap={'6px'}>
<Avatar src={currentOrg?.avatar} w={'1rem'} h={'1rem'} rounded={'xs'} />
<Box fontWeight={500} color={'myGray.900'}>
{currentOrg?.name}
</Box>
{currentOrg?.path !== '' && (
<IconButton name="edit" onClick={() => setEditOrg(currentOrg)} />
)}
</HStack>
<Box fontSize={'xs'}>{currentOrg?.description || t('common:common.no_intro')}</Box>
<Divider my={'20px'} />
<Box fontWeight={500} fontSize="sm" color="myGray.900">
{t('common:common.Action')}
</Box>
{currentOrg && isTeamAdmin && (
<VStack gap="13px" w="100%">
<ActionButton
icon="common/add2"
text={t('account_team:create_sub_org')}
onClick={() => {
setEditOrg({
...defaultOrgForm,
parentId: currentOrg?._id
});
}}
/>
<ActionButton
icon="common/administrator"
text={t('account_team:manage_member')}
onClick={() => setManageMemberOrg(currentOrg)}
/>
{currentOrg?.path !== '' && (
<>
<ActionButton
icon="common/file/move"
text={t('account_team:move_org')}
onClick={() => setMovingOrg(currentOrg)}
/>
<ActionButton
icon="delete"
text={t('account_team:delete_org')}
onClick={() => deleteOrgHandler(currentOrg._id)}
/>
</>
)}
</VStack>
)}
</VStack>
)}
</Flex>
</MyBox>
{!!editOrg && (
<OrgInfoModal
editOrg={editOrg}
onClose={() => setEditOrg(undefined)}
onSuccess={refetchOrgs}
/>
)}
{!!movingOrg && (
<OrgMoveModal
orgs={orgs}
movingOrg={movingOrg}
onClose={() => setMovingOrg(undefined)}
onSuccess={refetchOrgs}
/>
)}
{!!manageMemberOrg && (
<OrgMemberManageModal
currentOrg={manageMemberOrg}
refetchOrgs={refetchOrgs}
onClose={() => setManageMemberOrg(undefined)}
/>
)}
<ConfirmDeleteOrgModal />
<ConfirmDeleteMember />
</>
);
}
export default OrgTable;

View File

@ -24,7 +24,7 @@ import {
import { useUserStore } from '@/web/support/user/useUserStore'; import { useUserStore } from '@/web/support/user/useUserStore';
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip'; import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
import Avatar from '@fastgpt/web/components/common/Avatar'; import Avatar from '@fastgpt/web/components/common/Avatar';
import MemberTag from '../../../../../components/support/user/team/Info/MemberTag'; import MemberTag from '../../../../components/support/user/team/Info/MemberTag';
import { DefaultGroupName } from '@fastgpt/global/support/user/team/group/constant'; import { DefaultGroupName } from '@fastgpt/global/support/user/team/group/constant';
import { import {
TeamManagePermissionVal, TeamManagePermissionVal,

View File

@ -2,7 +2,7 @@ import React, { ReactNode, useState } from 'react';
import { createContext } from 'use-context-selector'; import { createContext } from 'use-context-selector';
import type { EditTeamFormDataType } from './EditInfoModal'; import type { EditTeamFormDataType } from './EditInfoModal';
import dynamic from 'next/dynamic'; import dynamic from 'next/dynamic';
import { getTeamList, putSwitchTeam } from '@/web/support/user/team/api'; import { getTeamList, getTeamMembers, putSwitchTeam } from '@/web/support/user/team/api';
import { TeamMemberStatusEnum } from '@fastgpt/global/support/user/team/constant'; import { TeamMemberStatusEnum } from '@fastgpt/global/support/user/team/constant';
import { useUserStore } from '@/web/support/user/useUserStore'; import { useUserStore } from '@/web/support/user/useUserStore';
import type { TeamTmbItemType, TeamMemberItemType } from '@fastgpt/global/support/user/team/type'; import type { TeamTmbItemType, TeamMemberItemType } from '@fastgpt/global/support/user/team/type';
@ -10,7 +10,7 @@ import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { useTranslation } from 'next-i18next'; import { useTranslation } from 'next-i18next';
import { getGroupList } from '@/web/support/user/team/group/api'; import { getGroupList } from '@/web/support/user/team/group/api';
import { MemberGroupListType } from '@fastgpt/global/support/permission/memberGroup/type'; import { MemberGroupListType } from '@fastgpt/global/support/permission/memberGroup/type';
import { OrgType } from '@fastgpt/global/support/user/team/org/type'; import { useScrollPagination } from '@fastgpt/web/hooks/useScrollPagination';
const EditInfoModal = dynamic(() => import('./EditInfoModal')); const EditInfoModal = dynamic(() => import('./EditInfoModal'));
@ -26,6 +26,7 @@ type TeamModalContextType = {
refetchTeams: () => void; refetchTeams: () => void;
refetchGroups: () => void; refetchGroups: () => void;
teamSize: number; teamSize: number;
MemberScrollData: ReturnType<typeof useScrollPagination>['ScrollData'];
}; };
export const TeamContext = createContext<TeamModalContextType>({ export const TeamContext = createContext<TeamModalContextType>({
@ -49,13 +50,14 @@ export const TeamContext = createContext<TeamModalContextType>({
throw new Error('Function not implemented.'); throw new Error('Function not implemented.');
}, },
teamSize: 0 teamSize: 0,
MemberScrollData: () => <></>
}); });
export const TeamModalContextProvider = ({ children }: { children: ReactNode }) => { export const TeamModalContextProvider = ({ children }: { children: ReactNode }) => {
const { t } = useTranslation(); const { t } = useTranslation();
const [editTeamData, setEditTeamData] = useState<EditTeamFormDataType>(); const [editTeamData, setEditTeamData] = useState<EditTeamFormDataType>();
const { userInfo, initUserInfo, loadAndGetTeamMembers } = useUserStore(); const { userInfo, initUserInfo } = useUserStore();
const { const {
data: myTeams = [], data: myTeams = [],
@ -69,18 +71,11 @@ export const TeamModalContextProvider = ({ children }: { children: ReactNode })
// member action // member action
const { const {
data: members = [], data: members = [],
runAsync: refetchMembers, isLoading: loadingMembers,
loading: loadingMembers refreshList: refetchMembers,
} = useRequest2( total: memberTotal,
() => { ScrollData: MemberScrollData
if (!userInfo?.team?.teamId) return Promise.resolve([]); } = useScrollPagination(getTeamMembers, {});
return loadAndGetTeamMembers(true);
},
{
manual: false,
refreshDeps: [userInfo?.team?.teamId]
}
);
const { runAsync: onSwitchTeam, loading: isSwitchingTeam } = useRequest2( const { runAsync: onSwitchTeam, loading: isSwitchingTeam } = useRequest2(
async (teamId: string) => { async (teamId: string) => {
@ -115,7 +110,8 @@ export const TeamModalContextProvider = ({ children }: { children: ReactNode })
refetchMembers, refetchMembers,
groups, groups,
refetchGroups, refetchGroups,
teamSize: members.length teamSize: memberTotal,
MemberScrollData
}; };
return ( return (

View File

@ -1,4 +1,4 @@
import React, { useState, useCallback, useMemo, useEffect } from 'react'; import React, { useState, useMemo, useEffect } from 'react';
import { import {
Button, Button,
Table, Table,
@ -25,7 +25,6 @@ import {
billStatusMap, billStatusMap,
billTypeMap billTypeMap
} from '@fastgpt/global/support/wallet/bill/constants'; } from '@fastgpt/global/support/wallet/bill/constants';
// import { usePagination } from '@/web/common/hooks/usePagination';
import MyBox from '@fastgpt/web/components/common/MyBox'; import MyBox from '@fastgpt/web/components/common/MyBox';
import { useRequest } from '@fastgpt/web/hooks/useRequest'; import { useRequest } from '@fastgpt/web/hooks/useRequest';
import { standardSubLevelMap, subModeMap } from '@fastgpt/global/support/wallet/sub/constants'; import { standardSubLevelMap, subModeMap } from '@fastgpt/global/support/wallet/sub/constants';
@ -33,25 +32,23 @@ import MySelect from '@fastgpt/web/components/common/MySelect';
import MyModal from '@fastgpt/web/components/common/MyModal'; import MyModal from '@fastgpt/web/components/common/MyModal';
import { usePagination } from '@fastgpt/web/hooks/usePagination'; import { usePagination } from '@fastgpt/web/hooks/usePagination';
import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel'; import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel';
import { useI18n } from '@/web/context/I18n';
const BillTable = () => { const BillTable = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const { commonT } = useI18n();
const { toast } = useToast(); const { toast } = useToast();
const [billType, setBillType] = useState<BillTypeEnum | ''>(''); const [billType, setBillType] = useState<BillTypeEnum | undefined>(undefined);
const [billDetail, setBillDetail] = useState<BillSchemaType>(); const [billDetail, setBillDetail] = useState<BillSchemaType>();
const billTypeList = useMemo( const billTypeList = useMemo(
() => () =>
[ [
{ label: t('account_bill:all'), value: '' }, { label: t('account_bill:all'), value: undefined },
...Object.entries(billTypeMap).map(([key, value]) => ({ ...Object.entries(billTypeMap).map(([key, value]) => ({
label: t(value.label as any), label: t(value.label as any),
value: key value: key
})) }))
] as { ] as {
label: string; label: string;
value: BillTypeEnum | ''; value: BillTypeEnum | undefined;
}[], }[],
[t] [t]
); );
@ -62,8 +59,7 @@ const BillTable = () => {
Pagination, Pagination,
getData, getData,
total total
} = usePagination({ } = usePagination(getBills, {
api: getBills,
pageSize: 20, pageSize: 20,
params: { params: {
type: billType type: billType
@ -110,7 +106,7 @@ const BillTable = () => {
<Tr> <Tr>
<Th>#</Th> <Th>#</Th>
<Th> <Th>
<MySelect<BillTypeEnum | ''> <MySelect
list={billTypeList} list={billTypeList}
value={billType} value={billType}
size={'sm'} size={'sm'}
@ -181,7 +177,6 @@ export default BillTable;
function BillDetailModal({ bill, onClose }: { bill: BillSchemaType; onClose: () => void }) { function BillDetailModal({ bill, onClose }: { bill: BillSchemaType; onClose: () => void }) {
const { t } = useTranslation(); const { t } = useTranslation();
const { commonT } = useI18n();
return ( return (
<MyModal <MyModal
isOpen={true} isOpen={true}

View File

@ -1,7 +1,7 @@
import { getInvoiceRecords } from '@/web/support/wallet/bill/invoice/api'; import { getInvoiceRecords } from '@/web/support/wallet/bill/invoice/api';
import MyBox from '@fastgpt/web/components/common/MyBox'; import MyBox from '@fastgpt/web/components/common/MyBox';
import { useTranslation } from 'next-i18next'; import { useTranslation } from 'next-i18next';
import { useEffect, useState } from 'react'; import { useState } from 'react';
import { import {
Box, Box,
Button, Button,
@ -30,8 +30,7 @@ const InvoiceTable = () => {
isLoading, isLoading,
Pagination, Pagination,
total total
} = usePagination({ } = usePagination(getInvoiceRecords, {
api: getInvoiceRecords,
pageSize: 20 pageSize: 20
}); });

View File

@ -1,4 +1,4 @@
import React, { useCallback, useMemo, useRef } from 'react'; import React, { useCallback, useMemo } from 'react';
import { import {
Box, Box,
Flex, Flex,
@ -160,6 +160,7 @@ const MyInfo = ({ onOpenContact }: { onOpenContact: () => void }) => {
color: 'myGray.900' color: 'myGray.900'
}; };
const isSyncMember = feConfigs.register_method?.includes('sync');
return ( return (
<Box> <Box>
{/* user info */} {/* user info */}
@ -224,6 +225,7 @@ const MyInfo = ({ onOpenContact }: { onOpenContact: () => void }) => {
<Box {...labelStyles}>{t('account_info:member_name')}:&nbsp;</Box> <Box {...labelStyles}>{t('account_info:member_name')}:&nbsp;</Box>
<Input <Input
flex={'1 0 0'} flex={'1 0 0'}
disabled={isSyncMember}
defaultValue={userInfo?.team?.memberName || 'Member'} defaultValue={userInfo?.team?.memberName || 'Member'}
title={t('account_info:click_modify_nickname')} title={t('account_info:click_modify_nickname')}
borderColor={'transparent'} borderColor={'transparent'}
@ -590,11 +592,6 @@ const Other = ({ onOpenContact }: { onOpenContact: () => void }) => {
const { feConfigs } = useSystemStore(); const { feConfigs } = useSystemStore();
const { t } = useTranslation(); const { t } = useTranslation();
const { isPc } = useSystem(); const { isPc } = useSystem();
const { userInfo, updateUserInfo } = useUserStore();
const { reset } = useForm<UserUpdateParams>({
defaultValues: userInfo as UserType
});
return ( return (
<Box> <Box>

View File

@ -1,13 +1,12 @@
import React from 'react'; import React from 'react';
import { Box, Button, Flex, useTheme } from '@chakra-ui/react'; import { Box, Button, Flex, useTheme } from '@chakra-ui/react';
import { getInforms, readInform } from '@/web/support/user/inform/api'; import { getInforms, readInform } from '@/web/support/user/inform/api';
import type { UserInformSchema } from '@fastgpt/global/support/user/inform/type';
import { formatTimeToChatTime } from '@fastgpt/global/common/string/time'; import { formatTimeToChatTime } from '@fastgpt/global/common/string/time';
import { usePagination } from '@fastgpt/web/hooks/usePagination'; import { usePagination } from '@fastgpt/web/hooks/usePagination';
import { useLoading } from '@fastgpt/web/hooks/useLoading'; import { useLoading } from '@fastgpt/web/hooks/useLoading';
import { useTranslation } from 'next-i18next'; import { useTranslation } from 'next-i18next';
import EmptyTip from '@fastgpt/web/components/common/EmptyTip'; import EmptyTip from '@fastgpt/web/components/common/EmptyTip';
import AccountContainer, { TabEnum } from './components/AccountContainer'; import AccountContainer from './components/AccountContainer';
import { serviceSideProps } from '@fastgpt/web/common/system/nextjs'; import { serviceSideProps } from '@fastgpt/web/common/system/nextjs';
const InformTable = () => { const InformTable = () => {
@ -23,8 +22,7 @@ const InformTable = () => {
Pagination, Pagination,
getData, getData,
pageNum pageNum
} = usePagination<UserInformSchema>({ } = usePagination(getInforms, {
api: getInforms,
pageSize: 20 pageSize: 20
}); });

View File

@ -25,7 +25,7 @@ import { usePagination } from '@fastgpt/web/hooks/usePagination';
import { useLoading } from '@fastgpt/web/hooks/useLoading'; import { useLoading } from '@fastgpt/web/hooks/useLoading';
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip'; import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
import EmptyTip from '@fastgpt/web/components/common/EmptyTip'; import EmptyTip from '@fastgpt/web/components/common/EmptyTip';
import AccountContainer, { TabEnum } from './components/AccountContainer'; import AccountContainer from './components/AccountContainer';
import { serviceSideProps } from '@fastgpt/web/common/system/nextjs'; import { serviceSideProps } from '@fastgpt/web/common/system/nextjs';
const Promotion = () => { const Promotion = () => {
@ -41,8 +41,7 @@ const Promotion = () => {
total, total,
pageSize, pageSize,
Pagination Pagination
} = usePagination({ } = usePagination(getPromotionRecords, {
api: getPromotionRecords,
pageSize: 20 pageSize: 20
}); });

View File

@ -1,354 +0,0 @@
import { deleteOrg, deleteOrgMember } from '@/web/support/user/team/org/api';
import { useUserStore } from '@/web/support/user/useUserStore';
import {
Box,
Divider,
Flex,
HStack,
Table,
TableContainer,
Tag,
Tbody,
Td,
Th,
Thead,
Tr,
VStack
} from '@chakra-ui/react';
import type { OrgType } from '@fastgpt/global/support/user/team/org/type';
import Avatar from '@fastgpt/web/components/common/Avatar';
import MyIcon from '@fastgpt/web/components/common/Icon';
import type { IconNameType } from '@fastgpt/web/components/common/Icon/type';
import MyMenu from '@fastgpt/web/components/common/MyMenu';
import { useConfirm } from '@fastgpt/web/hooks/useConfirm';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { useTranslation } from 'next-i18next';
import { useMemo, useState } from 'react';
import { useContextSelector } from 'use-context-selector';
import MemberTag from '@/components/support/user/team/Info/MemberTag';
import { TeamContext } from '../context';
import { getOrgList } from '@/web/support/user/team/org/api';
import IconButton from './IconButton';
import { defaultOrgForm, type OrgFormType } from './OrgInfoModal';
import dynamic from 'next/dynamic';
import MyBox from '@fastgpt/web/components/common/MyBox';
import Path from '@/components/common/folder/Path';
import { ParentTreePathItemType } from '@fastgpt/global/common/parentFolder/type';
import { getOrgChildrenPath } from '@fastgpt/global/support/user/team/org/constant';
const OrgInfoModal = dynamic(() => import('./OrgInfoModal'));
const OrgMemberManageModal = dynamic(() => import('./OrgMemberManageModal'));
const OrgMoveModal = dynamic(() => import('./OrgMoveModal'));
function ActionButton({
icon,
text,
onClick
}: {
icon: IconNameType;
text: string;
onClick: () => void;
}) {
return (
<HStack
gap={'8px'}
w="100%"
transition={'background 0.1s'}
cursor={'pointer'}
p="4px"
rounded={'sm'}
_hover={{
bg: 'myGray.05',
color: 'primary.600'
}}
onClick={onClick}
>
<MyIcon name={icon} w="1rem" h="1rem" />
<Box fontSize={'sm'}>{text}</Box>
</HStack>
);
}
function OrgTable({ Tabs }: { Tabs: React.ReactNode }) {
const { t } = useTranslation();
const { userInfo, isTeamAdmin } = useUserStore();
const { members } = useContextSelector(TeamContext, (v) => v);
const [parentPath, setParentPath] = useState('');
const {
data: orgs = [],
loading: isLoadingOrgs,
refresh: refetchOrgs
} = useRequest2(getOrgList, {
manual: false,
refreshDeps: [userInfo?.team?.teamId]
});
const currentOrgs = useMemo(() => {
if (orgs.length === 0) return [];
// Auto select the first org(root org is team)
if (parentPath === '') {
setParentPath(getOrgChildrenPath(orgs[0]));
return [];
}
return orgs
.filter((org) => org.path === parentPath)
.map((item) => {
return {
...item,
// Member + org
count:
item.members.length + orgs.filter((org) => org.path === getOrgChildrenPath(item)).length
};
});
}, [orgs, parentPath]);
const currentOrg = useMemo(() => {
const splitPath = parentPath.split('/');
const currentOrgId = splitPath[splitPath.length - 1];
if (!currentOrgId) return;
return orgs.find((org) => org.pathId === currentOrgId);
}, [orgs, parentPath]);
const paths = useMemo(() => {
const splitPath = parentPath.split('/').filter(Boolean);
return splitPath
.map((id) => {
const org = orgs.find((org) => org.pathId === id)!;
if (org.path === '') return;
return {
parentId: getOrgChildrenPath(org),
parentName: org.name
};
})
.filter(Boolean) as ParentTreePathItemType[];
}, [parentPath, orgs]);
const [editOrg, setEditOrg] = useState<OrgFormType>();
const [manageMemberOrg, setManageMemberOrg] = useState<OrgType>();
const [movingOrg, setMovingOrg] = useState<OrgType>();
// Delete org
const { ConfirmModal: ConfirmDeleteOrgModal, openConfirm: openDeleteOrgModal } = useConfirm({
type: 'delete',
content: t('account_team:confirm_delete_org')
});
const deleteOrgHandler = (orgId: string) => openDeleteOrgModal(() => deleteOrgReq(orgId))();
const { runAsync: deleteOrgReq } = useRequest2(deleteOrg, {
onSuccess: () => {
refetchOrgs();
}
});
// Delete member
const { ConfirmModal: ConfirmDeleteMember, openConfirm: openDeleteMemberModal } = useConfirm({
type: 'delete',
content: t('account_team:confirm_delete_member')
});
const { runAsync: deleteMemberReq } = useRequest2(deleteOrgMember, {
onSuccess: () => {
refetchOrgs();
}
});
return (
<>
<Flex justify={'space-between'} align={'center'} pb={'1rem'}>
{Tabs}
</Flex>
<MyBox flex={'1 0 0'} overflow={'auto'} isLoading={isLoadingOrgs}>
<Box mb={3}>
<Path paths={paths} rootName={userInfo?.team?.teamName} onClick={setParentPath} />
</Box>
<Flex w={'100%'} gap={'4'}>
{/* Table */}
<TableContainer overflow={'unset'} fontSize={'sm'} flexGrow={1}>
<Table overflow={'unset'}>
<Thead>
<Tr bg={'white !important'}>
<Th bg="myGray.100" borderLeftRadius="6px">
{t('common:Name')}
</Th>
<Th bg="myGray.100" borderRightRadius="6px">
{t('common:common.Action')}
</Th>
</Tr>
</Thead>
<Tbody>
{currentOrgs.map((org) => (
<Tr key={org._id} overflow={'unset'}>
<Td>
<HStack
cursor={'pointer'}
onClick={() => setParentPath(getOrgChildrenPath(org))}
>
<MemberTag name={org.name} avatar={org.avatar} />
<Tag size="sm">{org.count}</Tag>
<MyIcon
name="core/chat/chevronRight"
w={'1rem'}
h={'1rem'}
color={'myGray.500'}
/>
</HStack>
</Td>
<Td w={'6rem'}>
{isTeamAdmin && (
<MyMenu
trigger="hover"
Button={<IconButton name="more" />}
menuList={[
{
children: [
{
icon: 'edit',
label: t('account_team:edit_info'),
onClick: () => setEditOrg(org)
},
{
icon: 'common/file/move',
label: t('common:Move'),
onClick: () => setMovingOrg(org)
},
{
icon: 'delete',
label: t('account_team:delete'),
type: 'danger',
onClick: () => deleteOrgHandler(org._id)
}
]
}
]}
/>
)}
</Td>
</Tr>
))}
{currentOrg?.members.map((member) => {
const memberInfo = members.find((m) => m.tmbId === member.tmbId);
if (!memberInfo) return null;
return (
<Tr key={member.tmbId}>
<Td>
<MemberTag name={memberInfo.memberName} avatar={memberInfo.avatar} />
</Td>
<Td w={'6rem'}>
{isTeamAdmin && (
<MyMenu
trigger={'hover'}
Button={<IconButton name="more" />}
menuList={[
{
children: [
{
icon: 'delete',
label: t('account_team:delete'),
type: 'danger',
onClick: () =>
openDeleteMemberModal(() =>
deleteMemberReq(currentOrg._id, member.tmbId)
)()
}
]
}
]}
/>
)}
</Td>
</Tr>
);
})}
</Tbody>
</Table>
</TableContainer>
{/* Slider */}
<VStack w={'180px'} alignItems={'start'}>
<HStack gap={'6px'}>
<Avatar src={currentOrg?.avatar} w={'1rem'} h={'1rem'} rounded={'xs'} />
<Box fontWeight={500} color={'myGray.900'}>
{currentOrg?.name}
</Box>
{currentOrg?.path !== '' && (
<IconButton name="edit" onClick={() => setEditOrg(currentOrg)} />
)}
</HStack>
<Box fontSize={'xs'}>{currentOrg?.description || t('common:common.no_intro')}</Box>
<Divider my={'20px'} />
<Box fontWeight={500} fontSize="sm" color="myGray.900">
{t('common:common.Action')}
</Box>
{currentOrg && isTeamAdmin && (
<VStack gap="13px" w="100%">
<ActionButton
icon="common/add2"
text={t('account_team:create_sub_org')}
onClick={() => {
setEditOrg({
...defaultOrgForm,
parentId: currentOrg?._id
});
}}
/>
<ActionButton
icon="common/administrator"
text={t('account_team:manage_member')}
onClick={() => setManageMemberOrg(currentOrg)}
/>
{currentOrg?.path !== '' && (
<>
<ActionButton
icon="common/file/move"
text={t('account_team:move_org')}
onClick={() => setMovingOrg(currentOrg)}
/>
<ActionButton
icon="delete"
text={t('account_team:delete_org')}
onClick={() => deleteOrgHandler(currentOrg._id)}
/>
</>
)}
</VStack>
)}
</VStack>
</Flex>
{!!editOrg && (
<OrgInfoModal
editOrg={editOrg}
onClose={() => setEditOrg(undefined)}
onSuccess={refetchOrgs}
/>
)}
{!!movingOrg && (
<OrgMoveModal
orgs={orgs}
movingOrg={movingOrg}
onClose={() => setMovingOrg(undefined)}
onSuccess={refetchOrgs}
/>
)}
{!!manageMemberOrg && (
<OrgMemberManageModal
currentOrg={manageMemberOrg}
refetchOrgs={refetchOrgs}
onClose={() => setManageMemberOrg(undefined)}
/>
)}
<ConfirmDeleteOrgModal />
<ConfirmDeleteMember />
</MyBox>
</>
);
}
export default OrgTable;

View File

@ -1,6 +1,6 @@
import { serviceSideProps } from '@fastgpt/web/common/system/nextjs'; import { serviceSideProps } from '@fastgpt/web/common/system/nextjs';
import AccountContainer from '../components/AccountContainer'; import AccountContainer from '../components/AccountContainer';
import { Box, Flex, useDisclosure } from '@chakra-ui/react'; import { Box, Flex } from '@chakra-ui/react';
import Icon from '@fastgpt/web/components/common/Icon'; import Icon from '@fastgpt/web/components/common/Icon';
import { useTranslation } from 'next-i18next'; import { useTranslation } from 'next-i18next';
import TeamSelector from '../components/TeamSelector'; import TeamSelector from '../components/TeamSelector';
@ -11,14 +11,15 @@ import { useRouter } from 'next/router';
import FillRowTabs from '@fastgpt/web/components/common/Tabs/FillRowTabs'; import FillRowTabs from '@fastgpt/web/components/common/Tabs/FillRowTabs';
import MyIcon from '@fastgpt/web/components/common/Icon'; import MyIcon from '@fastgpt/web/components/common/Icon';
import { TeamMemberRoleEnum } from '@fastgpt/global/support/user/team/constant'; import { TeamMemberRoleEnum } from '@fastgpt/global/support/user/team/constant';
import { TeamContext, TeamModalContextProvider } from './components/context'; import { TeamContext, TeamModalContextProvider } from '@/pageComponents/account/team/context';
import dynamic from 'next/dynamic'; import dynamic from 'next/dynamic';
import MemberTable from './components/MemberTable';
const PermissionManage = dynamic(() => import('./components/PermissionManage/index')); const MemberTable = dynamic(() => import('@/pageComponents/account/team/MemberTable'));
const GroupManage = dynamic(() => import('./components/GroupManage/index')); const PermissionManage = dynamic(
() => import('@/pageComponents/account/team/PermissionManage/index')
const OrgManage = dynamic(() => import('./components/OrgManage/index')); );
const GroupManage = dynamic(() => import('@/pageComponents/account/team/GroupManage/index'));
const OrgManage = dynamic(() => import('@/pageComponents/account/team/OrgManage/index'));
export enum TeamTabEnum { export enum TeamTabEnum {
member = 'member', member = 'member',
@ -34,7 +35,7 @@ const Team = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const { userInfo } = useUserStore(); const { userInfo } = useUserStore();
const { setEditTeamData, teamSize, isLoading } = useContextSelector(TeamContext, (v) => v); const { setEditTeamData, isLoading, teamSize } = useContextSelector(TeamContext, (v) => v);
const Tabs = useMemo( const Tabs = useMemo(
() => ( () => (
@ -62,72 +63,81 @@ const Team = () => {
return ( return (
<AccountContainer isLoading={isLoading}> <AccountContainer isLoading={isLoading}>
{/* header */} <Flex h={'100%'} flexDirection={'column'}>
<Flex {/* header */}
w={'100%'} <Flex
h={'3.5rem'} w={'100%'}
px={'1.56rem'} h={'3.5rem'}
py={'0.56rem'} px={'1.56rem'}
borderBottom={'1px solid'} py={'0.56rem'}
borderColor={'myGray.200'} borderBottom={'1px solid'}
bg={'myGray.25'} borderColor={'myGray.200'}
align={'center'} bg={'myGray.25'}
gap={6} align={'center'}
justify={'space-between'} gap={6}
> justify={'space-between'}
<Flex align={'center'}> >
<Flex gap={2} color={'myGray.900'}> <Flex align={'center'}>
<Icon name="support/user/usersLight" w={'1.25rem'} h={'1.25rem'} /> <Flex gap={2} color={'myGray.900'}>
<Box fontWeight={'500'} fontSize={'1rem'}> <Icon name="support/user/usersLight" w={'1.25rem'} h={'1.25rem'} />
{t('account:team')} <Box fontWeight={'500'} fontSize={'1rem'}>
</Box> {t('account:team')}
</Flex> </Box>
<Flex align={'center'} ml={6}>
<TeamSelector height={'28px'} />
</Flex>
{userInfo?.team?.role === TeamMemberRoleEnum.owner && (
<Flex align={'center'} justify={'center'} ml={2} p={'0.44rem'}>
<MyIcon
name="edit"
w="18px"
cursor="pointer"
_hover={{
color: 'primary.500'
}}
onClick={() => {
if (!userInfo?.team) return;
setEditTeamData({
id: userInfo.team.teamId,
name: userInfo.team.teamName,
avatar: userInfo.team.avatar
});
}}
/>
</Flex> </Flex>
)} <Flex align={'center'} ml={6}>
<TeamSelector height={'28px'} />
</Flex>
{userInfo?.team?.role === TeamMemberRoleEnum.owner && (
<Flex align={'center'} justify={'center'} ml={2} p={'0.44rem'}>
<MyIcon
name="edit"
w="18px"
cursor="pointer"
_hover={{
color: 'primary.500'
}}
onClick={() => {
if (!userInfo?.team) return;
setEditTeamData({
id: userInfo.team.teamId,
name: userInfo.team.teamName,
avatar: userInfo.team.avatar
});
}}
/>
</Flex>
)}
</Flex>
<Box
float={'right'}
color={'myGray.900'}
h={'1.25rem'}
px={'0.5rem'}
py={'0.125rem'}
fontSize={'0.75rem'}
borderRadius={'1.25rem'}
bg={'myGray.150'}
>
{t('account_team:total_team_members', { amount: teamSize })}
</Box>
</Flex> </Flex>
{/* table */}
<Box <Box
float={'right'} py={'1.5rem'}
color={'myGray.900'} px={'2rem'}
h={'1.25rem'} flex={'1 0 0'}
px={'0.5rem'} display={'flex'}
py={'0.125rem'} flexDirection={'column'}
fontSize={'0.75rem'} overflow={'auto'}
borderRadius={'1.25rem'}
bg={'myGray.150'}
> >
{t('account_team:total_team_members', { amount: teamSize })} {teamTab === TeamTabEnum.member && <MemberTable Tabs={Tabs} />}
{teamTab === TeamTabEnum.org && <OrgManage Tabs={Tabs} />}
{teamTab === TeamTabEnum.group && <GroupManage Tabs={Tabs} />}
{teamTab === TeamTabEnum.permission && <PermissionManage Tabs={Tabs} />}
</Box> </Box>
</Flex> </Flex>
{/* table */}
<Box py={'1.5rem'} px={'2rem'}>
{teamTab === TeamTabEnum.member && <MemberTable Tabs={Tabs} />}
{teamTab === TeamTabEnum.org && <OrgManage Tabs={Tabs} />}
{teamTab === TeamTabEnum.group && <GroupManage Tabs={Tabs} />}
{teamTab === TeamTabEnum.permission && <PermissionManage Tabs={Tabs} />}
</Box>
</AccountContainer> </AccountContainer>
); );
}; };

View File

@ -23,15 +23,16 @@ import DateRangePicker, {
import { addDays } from 'date-fns'; import { addDays } from 'date-fns';
import dynamic from 'next/dynamic'; import dynamic from 'next/dynamic';
import { useTranslation } from 'next-i18next'; import { useTranslation } from 'next-i18next';
import { useQuery } from '@tanstack/react-query';
import { useUserStore } from '@/web/support/user/useUserStore'; import { useUserStore } from '@/web/support/user/useUserStore';
import Avatar from '@fastgpt/web/components/common/Avatar'; import Avatar from '@fastgpt/web/components/common/Avatar';
import MySelect from '@fastgpt/web/components/common/MySelect'; import MySelect from '@fastgpt/web/components/common/MySelect';
import { formatNumber } from '@fastgpt/global/common/math/tools'; import { formatNumber } from '@fastgpt/global/common/math/tools';
import EmptyTip from '@fastgpt/web/components/common/EmptyTip'; import EmptyTip from '@fastgpt/web/components/common/EmptyTip';
import { useSystem } from '@fastgpt/web/hooks/useSystem'; import { useSystem } from '@fastgpt/web/hooks/useSystem';
import AccountContainer, { TabEnum } from '../components/AccountContainer'; import AccountContainer from '../components/AccountContainer';
import { serviceSideProps } from '@fastgpt/web/common/system/nextjs'; import { serviceSideProps } from '@fastgpt/web/common/system/nextjs';
import { useScrollPagination } from '@fastgpt/web/hooks/useScrollPagination';
import { getTeamMembers } from '@/web/support/user/team/api';
const UsageDetail = dynamic(() => import('./UsageDetail')); const UsageDetail = dynamic(() => import('./UsageDetail'));
@ -44,7 +45,7 @@ const UsageTable = () => {
}); });
const [usageSource, setUsageSource] = useState<UsageSourceEnum | ''>(''); const [usageSource, setUsageSource] = useState<UsageSourceEnum | ''>('');
const { isPc } = useSystem(); const { isPc } = useSystem();
const { userInfo, loadAndGetTeamMembers } = useUserStore(); const { userInfo } = useUserStore();
const [usageDetail, setUsageDetail] = useState<UsageItemType>(); const [usageDetail, setUsageDetail] = useState<UsageItemType>();
const sourceList = useMemo( const sourceList = useMemo(
@ -63,10 +64,7 @@ const UsageTable = () => {
); );
const [selectTmbId, setSelectTmbId] = useState(userInfo?.team?.tmbId); const [selectTmbId, setSelectTmbId] = useState(userInfo?.team?.tmbId);
const { data: members = [] } = useQuery(['getMembers', userInfo?.team?.teamId], () => { const { data: members, ScrollData } = useScrollPagination(getTeamMembers, {});
if (!userInfo?.team?.teamId) return [];
return loadAndGetTeamMembers();
});
const tmbList = useMemo( const tmbList = useMemo(
() => () =>
members.map((item) => ({ members.map((item) => ({
@ -86,14 +84,13 @@ const UsageTable = () => {
isLoading, isLoading,
Pagination, Pagination,
getData getData
} = usePagination<UsageItemType>({ } = usePagination(getUserUsages, {
api: getUserUsages,
pageSize: isPc ? 20 : 10, pageSize: isPc ? 20 : 10,
params: { params: {
dateStart: dateRange.from || new Date(), dateStart: dateRange.from || new Date(),
dateEnd: addDays(dateRange.to || new Date(), 1), dateEnd: addDays(dateRange.to || new Date(), 1),
source: usageSource, source: usageSource as UsageSourceEnum,
teamMemberId: selectTmbId teamMemberId: selectTmbId ?? ''
}, },
defaultRequest: false defaultRequest: false
}); });
@ -120,6 +117,7 @@ const UsageTable = () => {
<MySelect <MySelect
size={'sm'} size={'sm'}
minW={'100px'} minW={'100px'}
ScrollData={ScrollData}
list={tmbList} list={tmbList}
value={selectTmbId} value={selectTmbId}
onchange={setSelectTmbId} onchange={setSelectTmbId}

View File

@ -5,6 +5,8 @@ import { jiebaSplit } from '@fastgpt/service/common/string/jieba';
import { MongoDatasetDataText } from '@fastgpt/service/core/dataset/data/dataTextSchema'; import { MongoDatasetDataText } from '@fastgpt/service/core/dataset/data/dataTextSchema';
import { MongoDatasetData } from '@fastgpt/service/core/dataset/data/schema'; import { MongoDatasetData } from '@fastgpt/service/core/dataset/data/schema';
import { authCert } from '@fastgpt/service/support/permission/auth/common'; import { authCert } from '@fastgpt/service/support/permission/auth/common';
import { MongoUser } from '@fastgpt/service/support/user/schema';
import { MongoTeamMember } from '@fastgpt/service/support/user/team/teamMemberSchema';
import { NextApiRequest, NextApiResponse } from 'next'; import { NextApiRequest, NextApiResponse } from 'next';
/* /*
@ -14,6 +16,7 @@ import { NextApiRequest, NextApiResponse } from 'next';
2. MongoDatasetData 2. MongoDatasetData
3. MongoDatasetDataText 3. MongoDatasetDataText
4. MongoDatasetData 4819 4. MongoDatasetData 4819
5. User avatar TeamMember
*/ */
let success = 0; let success = 0;
async function handler(req: NextApiRequest, res: NextApiResponse) { async function handler(req: NextApiRequest, res: NextApiResponse) {
@ -109,15 +112,26 @@ const initData = async (batchSize: number) => {
} }
}; };
const batchUpdateFields = async (batchSize = 2000) => { // const batchUpdateFields = async (batchSize = 2000) => {
// Update in batches // // Find documents that still have these fields
await MongoDatasetData.updateMany( // const documents = await MongoDatasetData.find({ initFullText: { $exists: true } }, '_id')
{ initFullText: { $exists: true } }, // .limit(batchSize)
{ // .lean();
$unset: {
initFullText: 1, // if (documents.length === 0) return;
fullTextToken: 1
} // // Update in batches
} // await MongoDatasetData.updateMany(
); // { _id: { $in: documents.map((doc) => doc._id) } },
}; // {
// $unset: {
// initFullText: 1
// // fullTextToken: 1
// }
// }
// );
// success += documents.length;
// console.log('Delete success:', success);
// await batchUpdateFields(batchSize);
// };

View File

@ -0,0 +1,55 @@
import { NextAPI } from '@/service/middleware/entry';
import { mongoSessionRun } from '@fastgpt/service/common/mongo/sessionRun';
import { authCert } from '@fastgpt/service/support/permission/auth/common';
import { MongoUser } from '@fastgpt/service/support/user/schema';
import { MongoTeamMember } from '@fastgpt/service/support/user/team/teamMemberSchema';
import { NextApiRequest, NextApiResponse } from 'next';
/*
MongoDatasetData
1. User avatar TeamMember
*/
async function handler(req: NextApiRequest, res: NextApiResponse) {
await authCert({ req, authRoot: true });
await moveUserAvatar();
return { success: true };
}
export default NextAPI(handler);
const moveUserAvatar = async () => {
try {
const users = await MongoUser.find({}, '_id avatar');
console.log('Total users:', users.length);
let success = 0;
for await (const user of users) {
// @ts-ignore
if (!user.avatar) continue;
try {
await mongoSessionRun(async (session) => {
await MongoTeamMember.updateOne(
{
userId: user._id
},
{
$set: {
avatar: (user as any).avatar // 删除 avatar 字段, 因为 Type 改了,所以这里不能直接写 user.avatar
}
},
{ session }
);
// @ts-ignore
user.avatar = undefined;
await user.save({ session });
});
success++;
console.log('Move avatar success:', success);
} catch (error) {
console.error(error);
}
}
} catch (error) {
console.error(error);
}
};

View File

@ -8,7 +8,8 @@ async function handler(req: ApiRequestProps<{}, { bufferId?: string }>, res: Nex
// If bufferId is the same as the current bufferId, return directly // If bufferId is the same as the current bufferId, return directly
if (bufferId && global.systemInitBufferId && global.systemInitBufferId === bufferId) { if (bufferId && global.systemInitBufferId && global.systemInitBufferId === bufferId) {
return { return {
bufferId: global.systemInitBufferId bufferId: global.systemInitBufferId,
systemVersion: global.systemVersion || '0.0.0'
}; };
} }

View File

@ -1,6 +1,5 @@
import type { NextApiRequest, NextApiResponse } from 'next'; import type { NextApiRequest, NextApiResponse } from 'next';
import { MongoChat } from '@fastgpt/service/core/chat/chatSchema'; import { MongoChat } from '@fastgpt/service/core/chat/chatSchema';
import type { PagingData } from '@/types';
import { AppLogsListItemType } from '@/types/app'; import { AppLogsListItemType } from '@/types/app';
import { Types } from '@fastgpt/service/common/mongo'; import { Types } from '@fastgpt/service/common/mongo';
import { addDays } from 'date-fns'; import { addDays } from 'date-fns';
@ -10,19 +9,22 @@ import { ChatItemCollectionName } from '@fastgpt/service/core/chat/chatItemSchem
import { NextAPI } from '@/service/middleware/entry'; import { NextAPI } from '@/service/middleware/entry';
import { WritePermissionVal } from '@fastgpt/global/support/permission/constant'; import { WritePermissionVal } from '@fastgpt/global/support/permission/constant';
import { readFromSecondary } from '@fastgpt/service/common/mongo/utils'; import { readFromSecondary } from '@fastgpt/service/common/mongo/utils';
import { parsePaginationRequest } from '@fastgpt/service/common/api/pagination';
import { PaginationResponse } from '@fastgpt/web/common/fetch/type';
import { addSourceMember } from '@fastgpt/service/support/user/utils';
async function handler( async function handler(
req: NextApiRequest, req: NextApiRequest,
_res: NextApiResponse _res: NextApiResponse
): Promise<PagingData<AppLogsListItemType>> { ): Promise<PaginationResponse<AppLogsListItemType>> {
const { const {
pageNum = 1,
pageSize = 20,
appId, appId,
dateStart = addDays(new Date(), -7), dateStart = addDays(new Date(), -7),
dateEnd = new Date() dateEnd = new Date()
} = req.body as GetAppChatLogsParams; } = req.body as GetAppChatLogsParams;
const { pageSize = 20, offset } = parsePaginationRequest(req);
if (!appId) { if (!appId) {
throw new Error('缺少参数'); throw new Error('缺少参数');
} }
@ -39,7 +41,7 @@ async function handler(
} }
}; };
const [data, total] = await Promise.all([ const [list, total] = await Promise.all([
MongoChat.aggregate( MongoChat.aggregate(
[ [
{ $match: where }, { $match: where },
@ -51,7 +53,7 @@ async function handler(
updateTime: -1 updateTime: -1
} }
}, },
{ $skip: (pageNum - 1) * pageSize }, { $skip: offset },
{ $limit: pageSize }, { $limit: pageSize },
{ {
$lookup: { $lookup: {
@ -144,10 +146,14 @@ async function handler(
MongoChat.countDocuments(where, { ...readFromSecondary }) MongoChat.countDocuments(where, { ...readFromSecondary })
]); ]);
const listWithSourceMember = await addSourceMember({
list: list
});
const listWithoutTmbId = list.filter((item) => !item.tmbId);
return { return {
pageNum, list: listWithSourceMember.concat(listWithoutTmbId),
pageSize,
data,
total total
}; };
} }

View File

@ -18,6 +18,7 @@ import { replaceRegChars } from '@fastgpt/global/common/string/tools';
import { concatPer } from '@fastgpt/service/support/permission/controller'; import { concatPer } from '@fastgpt/service/support/permission/controller';
import { getGroupsByTmbId } from '@fastgpt/service/support/permission/memberGroup/controllers'; import { getGroupsByTmbId } from '@fastgpt/service/support/permission/memberGroup/controllers';
import { getOrgIdSetWithParentByTmbId } from '@fastgpt/service/support/permission/org/controllers'; import { getOrgIdSetWithParentByTmbId } from '@fastgpt/service/support/permission/org/controllers';
import { addSourceMember } from '@fastgpt/service/support/user/utils';
export type ListAppBody = { export type ListAppBody = {
parentId?: ParentIdType; parentId?: ParentIdType;
@ -201,19 +202,9 @@ async function handler(req: ApiRequestProps<ListAppBody>): Promise<AppListItemTy
}) })
.filter((app) => app.permission.hasReadPer); .filter((app) => app.permission.hasReadPer);
return formatApps.map((app) => ({ return addSourceMember({
_id: app._id, list: formatApps
tmbId: app.tmbId, });
avatar: app.avatar,
type: app.type,
name: app.name,
intro: app.intro,
updateTime: app.updateTime,
permission: app.permission,
pluginData: app.pluginData,
inheritPermission: app.inheritPermission ?? true,
private: app.privateApp
}));
} }
export default NextAPI(handler); export default NextAPI(handler);

View File

@ -6,6 +6,8 @@ import { ApiRequestProps } from '@fastgpt/service/type/next';
import { authApp } from '@fastgpt/service/support/permission/app/auth'; import { authApp } from '@fastgpt/service/support/permission/app/auth';
import { WritePermissionVal } from '@fastgpt/global/support/permission/constant'; import { WritePermissionVal } from '@fastgpt/global/support/permission/constant';
import { VersionListItemType } from '@fastgpt/global/core/app/version'; import { VersionListItemType } from '@fastgpt/global/core/app/version';
import { parsePaginationRequest } from '@fastgpt/service/common/api/pagination';
import { addSourceMember } from '@fastgpt/service/support/user/utils';
export type versionListBody = PaginationProps<{ export type versionListBody = PaginationProps<{
appId: string; appId: string;
@ -15,41 +17,40 @@ export type versionListResponse = PaginationResponse<VersionListItemType>;
async function handler( async function handler(
req: ApiRequestProps<versionListBody>, req: ApiRequestProps<versionListBody>,
res: NextApiResponse<any> _res: NextApiResponse<any>
): Promise<versionListResponse> { ): Promise<versionListResponse> {
const { offset, pageSize, appId } = req.body; const { appId } = req.body;
const { offset, pageSize } = parsePaginationRequest(req);
await authApp({ appId, req, per: WritePermissionVal, authToken: true }); await authApp({ appId, req, per: WritePermissionVal, authToken: true });
const [result, total] = await Promise.all([ const [result, total] = await Promise.all([
MongoAppVersion.find( (async () => {
{ const versions = await MongoAppVersion.find({
appId appId
},
'_id appId versionName time isPublish tmbId'
)
.sort({
time: -1
}) })
.skip(offset) .sort({
.limit(pageSize), time: -1
})
.skip(offset)
.limit(pageSize)
.lean();
return addSourceMember({
list: versions
}).then((list) =>
list.map((item) => ({
...item,
isPublish: !!item.isPublish
}))
);
})(),
MongoAppVersion.countDocuments({ appId }) MongoAppVersion.countDocuments({ appId })
]); ]);
const versionList = result.map((item) => {
return {
_id: item._id,
appId: item.appId,
versionName: item.versionName,
time: item.time,
isPublish: item.isPublish,
tmbId: item.tmbId
};
});
return { return {
total, total,
list: versionList list: result
}; };
} }

View File

@ -12,7 +12,6 @@ describe('发布应用版本测试', () => {
nodes: [], nodes: [],
edges: [], edges: [],
chatConfig: {}, chatConfig: {},
type: AppTypeEnum.simple,
isPublish: false, isPublish: false,
versionName: '1' versionName: '1'
}; };

View File

@ -164,6 +164,10 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
runningAppInfo: { runningAppInfo: {
id: appId, id: appId,
teamId: app.teamId,
tmbId: app.tmbId
},
runningUserInfo: {
teamId, teamId,
tmbId tmbId
}, },

View File

@ -7,6 +7,7 @@ import { NextAPI } from '@/service/middleware/entry';
import { ApiRequestProps, ApiResponseType } from '@fastgpt/service/type/next'; import { ApiRequestProps, ApiResponseType } from '@fastgpt/service/type/next';
import { PaginationProps, PaginationResponse } from '@fastgpt/web/common/fetch/type'; import { PaginationProps, PaginationResponse } from '@fastgpt/web/common/fetch/type';
import { GetHistoriesProps } from '@/global/core/chat/api'; import { GetHistoriesProps } from '@/global/core/chat/api';
import { parsePaginationRequest } from '@fastgpt/service/common/api/pagination';
import { addMonths } from 'date-fns'; import { addMonths } from 'date-fns';
export type getHistoriesQuery = {}; export type getHistoriesQuery = {};
@ -17,9 +18,10 @@ export type getHistoriesResponse = {};
async function handler( async function handler(
req: ApiRequestProps<getHistoriesBody, getHistoriesQuery>, req: ApiRequestProps<getHistoriesBody, getHistoriesQuery>,
res: ApiResponseType<any> _res: ApiResponseType<any>
): Promise<PaginationResponse<getHistoriesResponse>> { ): Promise<PaginationResponse<getHistoriesResponse>> {
const { appId, shareId, outLinkUid, teamId, teamToken, offset, pageSize, source } = req.body; const { appId, shareId, outLinkUid, teamId, teamToken, source } = req.body;
const { offset, pageSize } = parsePaginationRequest(req);
const match = await (async () => { const match = await (async () => {
if (shareId && outLinkUid) { if (shareId && outLinkUid) {

View File

@ -13,6 +13,7 @@ import { filterPublicNodeResponseData } from '@fastgpt/global/core/chat/utils';
import { GetChatTypeEnum } from '@/global/core/chat/constants'; import { GetChatTypeEnum } from '@/global/core/chat/constants';
import { PaginationProps, PaginationResponse } from '@fastgpt/web/common/fetch/type'; import { PaginationProps, PaginationResponse } from '@fastgpt/web/common/fetch/type';
import { ChatItemType } from '@fastgpt/global/core/chat/type'; import { ChatItemType } from '@fastgpt/global/core/chat/type';
import { parsePaginationRequest } from '@fastgpt/service/common/api/pagination';
export type getPaginationRecordsQuery = {}; export type getPaginationRecordsQuery = {};
@ -22,16 +23,11 @@ export type getPaginationRecordsResponse = PaginationResponse<ChatItemType>;
async function handler( async function handler(
req: ApiRequestProps<getPaginationRecordsBody, getPaginationRecordsQuery>, req: ApiRequestProps<getPaginationRecordsBody, getPaginationRecordsQuery>,
res: ApiResponseType<any> _res: ApiResponseType<any>
): Promise<getPaginationRecordsResponse> { ): Promise<getPaginationRecordsResponse> {
const { const { appId, chatId, loadCustomFeedbacks, type = GetChatTypeEnum.normal } = req.body;
appId,
chatId, const { offset, pageSize } = parsePaginationRequest(req);
offset,
pageSize = 10,
loadCustomFeedbacks,
type = GetChatTypeEnum.normal
} = req.body;
if (!appId || !chatId) { if (!appId || !chatId) {
return { return {

View File

@ -6,6 +6,7 @@ import { ApiRequestProps } from '@fastgpt/service/type/next';
import { ChatInputGuideSchemaType } from '@fastgpt/global/core/chat/inputGuide/type'; import { ChatInputGuideSchemaType } from '@fastgpt/global/core/chat/inputGuide/type';
import { authApp } from '@fastgpt/service/support/permission/app/auth'; import { authApp } from '@fastgpt/service/support/permission/app/auth';
import { ReadPermissionVal } from '@fastgpt/global/support/permission/constant'; import { ReadPermissionVal } from '@fastgpt/global/support/permission/constant';
import { parsePaginationRequest } from '@fastgpt/service/common/api/pagination';
export type ChatInputGuideProps = PaginationProps<{ export type ChatInputGuideProps = PaginationProps<{
appId: string; appId: string;
@ -17,7 +18,8 @@ async function handler(
req: ApiRequestProps<ChatInputGuideProps>, req: ApiRequestProps<ChatInputGuideProps>,
res: NextApiResponse<any> res: NextApiResponse<any>
): Promise<ChatInputGuideResponse> { ): Promise<ChatInputGuideResponse> {
const { appId, pageSize, offset, searchKey } = req.body; const { appId, searchKey } = req.body;
const { offset, pageSize } = parsePaginationRequest(req);
await authApp({ req, appId, authToken: true, per: ReadPermissionVal }); await authApp({ req, appId, authToken: true, per: ReadPermissionVal });

View File

@ -2,7 +2,6 @@ import type { NextApiRequest } from 'next';
import { DatasetTrainingCollectionName } from '@fastgpt/service/core/dataset/training/schema'; import { DatasetTrainingCollectionName } from '@fastgpt/service/core/dataset/training/schema';
import { Types } from '@fastgpt/service/common/mongo'; import { Types } from '@fastgpt/service/common/mongo';
import type { DatasetCollectionsListItemType } from '@/global/core/dataset/type.d'; import type { DatasetCollectionsListItemType } from '@/global/core/dataset/type.d';
import type { GetDatasetCollectionsProps } from '@/global/core/api/datasetReq';
import { MongoDatasetCollection } from '@fastgpt/service/core/dataset/collection/schema'; import { MongoDatasetCollection } from '@fastgpt/service/core/dataset/collection/schema';
import { DatasetCollectionTypeEnum } from '@fastgpt/global/core/dataset/constants'; import { DatasetCollectionTypeEnum } from '@fastgpt/global/core/dataset/constants';
import { authDataset } from '@fastgpt/service/support/permission/dataset/auth'; import { authDataset } from '@fastgpt/service/support/permission/dataset/auth';
@ -10,11 +9,10 @@ import { DatasetDataCollectionName } from '@fastgpt/service/core/dataset/data/sc
import { startTrainingQueue } from '@/service/core/dataset/training/utils'; import { startTrainingQueue } from '@/service/core/dataset/training/utils';
import { NextAPI } from '@/service/middleware/entry'; import { NextAPI } from '@/service/middleware/entry';
import { ReadPermissionVal } from '@fastgpt/global/support/permission/constant'; import { ReadPermissionVal } from '@fastgpt/global/support/permission/constant';
import { PagingData } from '@/types';
import { readFromSecondary } from '@fastgpt/service/common/mongo/utils'; import { readFromSecondary } from '@fastgpt/service/common/mongo/utils';
import { collectionTagsToTagLabel } from '@fastgpt/service/core/dataset/collection/utils'; import { collectionTagsToTagLabel } from '@fastgpt/service/core/dataset/collection/utils';
async function handler(req: NextApiRequest): Promise<PagingData<DatasetCollectionsListItemType>> { async function handler(req: NextApiRequest) {
let { let {
pageNum = 1, pageNum = 1,
pageSize = 10, pageSize = 10,
@ -24,7 +22,7 @@ async function handler(req: NextApiRequest): Promise<PagingData<DatasetCollectio
selectFolder = false, selectFolder = false,
filterTags = [], filterTags = [],
simple = false simple = false
} = req.body as GetDatasetCollectionsProps; } = req.body as any;
searchText = searchText?.replace(/'/g, ''); searchText = searchText?.replace(/'/g, '');
pageSize = Math.min(pageSize, 30); pageSize = Math.min(pageSize, 30);

View File

@ -0,0 +1,192 @@
import type { NextApiRequest } from 'next';
import { DatasetTrainingCollectionName } from '@fastgpt/service/core/dataset/training/schema';
import { Types } from '@fastgpt/service/common/mongo';
import type { DatasetCollectionsListItemType } from '@/global/core/dataset/type.d';
import type { GetDatasetCollectionsProps } from '@/global/core/api/datasetReq';
import { MongoDatasetCollection } from '@fastgpt/service/core/dataset/collection/schema';
import { DatasetCollectionTypeEnum } from '@fastgpt/global/core/dataset/constants';
import { authDataset } from '@fastgpt/service/support/permission/dataset/auth';
import { DatasetDataCollectionName } from '@fastgpt/service/core/dataset/data/schema';
import { startTrainingQueue } from '@/service/core/dataset/training/utils';
import { NextAPI } from '@/service/middleware/entry';
import { ReadPermissionVal } from '@fastgpt/global/support/permission/constant';
import { readFromSecondary } from '@fastgpt/service/common/mongo/utils';
import { collectionTagsToTagLabel } from '@fastgpt/service/core/dataset/collection/utils';
import { PaginationResponse } from '@fastgpt/web/common/fetch/type';
import { parsePaginationRequest } from '@fastgpt/service/common/api/pagination';
async function handler(
req: NextApiRequest
): Promise<PaginationResponse<DatasetCollectionsListItemType>> {
let {
datasetId,
parentId = null,
searchText = '',
selectFolder = false,
filterTags = [],
simple = false
} = req.body as GetDatasetCollectionsProps;
let { pageSize, offset } = parsePaginationRequest(req);
pageSize = Math.min(pageSize, 30);
searchText = searchText?.replace(/'/g, '');
// auth dataset and get my role
const { teamId, permission } = await authDataset({
req,
authToken: true,
authApiKey: true,
datasetId,
per: ReadPermissionVal
});
const match = {
teamId: new Types.ObjectId(teamId),
datasetId: new Types.ObjectId(datasetId),
parentId: parentId ? new Types.ObjectId(parentId) : null,
...(selectFolder ? { type: DatasetCollectionTypeEnum.folder } : {}),
...(searchText
? {
name: new RegExp(searchText, 'i')
}
: {}),
...(filterTags.length ? { tags: { $in: filterTags } } : {})
};
const selectField = {
_id: 1,
parentId: 1,
tmbId: 1,
name: 1,
type: 1,
forbid: 1,
createTime: 1,
updateTime: 1,
trainingType: 1,
fileId: 1,
rawLink: 1,
tags: 1,
externalFileId: 1
};
// not count data amount
if (simple) {
const collections = await MongoDatasetCollection.find(match, undefined, {
...readFromSecondary
})
.select(selectField)
.sort({
updateTime: -1
})
.lean();
return {
list: await Promise.all(
collections.map(async (item) => ({
...item,
tags: await collectionTagsToTagLabel({
datasetId,
tags: item.tags
}),
dataAmount: 0,
trainingAmount: 0,
permission
}))
),
total: await MongoDatasetCollection.countDocuments(match)
};
}
const [collections, total]: [DatasetCollectionsListItemType[], number] = await Promise.all([
MongoDatasetCollection.aggregate([
{
$match: match
},
{
$sort: { updateTime: -1 }
},
{
$skip: offset
},
{
$limit: pageSize
},
// count training data
{
$lookup: {
from: DatasetTrainingCollectionName,
let: { id: '$_id', team_id: match.teamId, dataset_id: match.datasetId },
pipeline: [
{
$match: {
$expr: {
$and: [{ $eq: ['$teamId', '$$team_id'] }, { $eq: ['$collectionId', '$$id'] }]
}
}
},
{ $count: 'count' }
],
as: 'trainingCount'
}
},
// count collection total data
{
$lookup: {
from: DatasetDataCollectionName,
let: { id: '$_id', team_id: match.teamId, dataset_id: match.datasetId },
pipeline: [
{
$match: {
$expr: {
$and: [
{ $eq: ['$teamId', '$$team_id'] },
{ $eq: ['$datasetId', '$$dataset_id'] },
{ $eq: ['$collectionId', '$$id'] }
]
}
}
},
{ $count: 'count' }
],
as: 'dataCount'
}
},
{
$project: {
...selectField,
dataAmount: {
$ifNull: [{ $arrayElemAt: ['$dataCount.count', 0] }, 0]
},
trainingAmount: {
$ifNull: [{ $arrayElemAt: ['$trainingCount.count', 0] }, 0]
}
}
}
]),
MongoDatasetCollection.countDocuments(match, {
...readFromSecondary
})
]);
const list = await Promise.all(
collections.map(async (item) => ({
...item,
tags: await collectionTagsToTagLabel({
datasetId,
tags: item.tags
}),
permission
}))
);
if (list.find((item) => item.trainingAmount > 0)) {
startTrainingQueue();
}
// count collections
return {
list,
total
};
}
export default NextAPI(handler);

View File

@ -10,6 +10,7 @@ import { CommonErrEnum } from '@fastgpt/global/common/error/code/common';
import { ApiRequestProps } from '@fastgpt/service/type/next'; import { ApiRequestProps } from '@fastgpt/service/type/next';
import { PaginationProps, PaginationResponse } from '@fastgpt/web/common/fetch/type'; import { PaginationProps, PaginationResponse } from '@fastgpt/web/common/fetch/type';
import type { DatasetCollectionsListItemType } from '@/global/core/dataset/type.d'; import type { DatasetCollectionsListItemType } from '@/global/core/dataset/type.d';
import { parsePaginationRequest } from '@fastgpt/service/common/api/pagination';
export type GetScrollCollectionsProps = PaginationProps<{ export type GetScrollCollectionsProps = PaginationProps<{
datasetId: string; datasetId: string;
@ -25,8 +26,6 @@ async function handler(
): Promise<PaginationResponse<DatasetCollectionsListItemType>> { ): Promise<PaginationResponse<DatasetCollectionsListItemType>> {
let { let {
datasetId, datasetId,
pageSize = 10,
offset,
parentId = null, parentId = null,
searchText = '', searchText = '',
selectFolder = false, selectFolder = false,
@ -36,6 +35,7 @@ async function handler(
if (!datasetId) { if (!datasetId) {
return Promise.reject(CommonErrEnum.missingParams); return Promise.reject(CommonErrEnum.missingParams);
} }
let { offset, pageSize } = parsePaginationRequest(req);
searchText = searchText?.replace(/'/g, ''); searchText = searchText?.replace(/'/g, '');
pageSize = Math.min(pageSize, 30); pageSize = Math.min(pageSize, 30);

View File

@ -3,19 +3,21 @@ import { MongoDatasetData } from '@fastgpt/service/core/dataset/data/schema';
import { replaceRegChars } from '@fastgpt/global/common/string/tools'; import { replaceRegChars } from '@fastgpt/global/common/string/tools';
import { NextAPI } from '@/service/middleware/entry'; import { NextAPI } from '@/service/middleware/entry';
import { ReadPermissionVal } from '@fastgpt/global/support/permission/constant'; import { ReadPermissionVal } from '@fastgpt/global/support/permission/constant';
import { PagingData, RequestPaging } from '@/types';
import { ApiRequestProps } from '@fastgpt/service/type/next'; import { ApiRequestProps } from '@fastgpt/service/type/next';
import { DatasetDataListItemType } from '@/global/core/dataset/type'; import { DatasetDataListItemType } from '@/global/core/dataset/type';
import { parsePaginationRequest } from '@fastgpt/service/common/api/pagination';
import { PaginationResponse } from '@fastgpt/web/common/fetch/type';
export type GetDatasetDataListProps = RequestPaging & { export type GetDatasetDataListProps = {
searchText?: string; searchText?: string;
collectionId: string; collectionId: string;
}; };
async function handler( async function handler(
req: ApiRequestProps<GetDatasetDataListProps> req: ApiRequestProps<GetDatasetDataListProps>
): Promise<PagingData<DatasetDataListItemType>> { ): Promise<PaginationResponse<DatasetDataListItemType>> {
let { pageNum = 1, pageSize = 10, searchText = '', collectionId } = req.body; let { searchText = '', collectionId } = req.body;
let { offset, pageSize } = parsePaginationRequest(req);
pageSize = Math.min(pageSize, 30); pageSize = Math.min(pageSize, 30);
@ -40,19 +42,17 @@ async function handler(
: {}) : {})
}; };
const [data, total] = await Promise.all([ const [list, total] = await Promise.all([
MongoDatasetData.find(match, '_id datasetId collectionId q a chunkIndex') MongoDatasetData.find(match, '_id datasetId collectionId q a chunkIndex')
.sort({ chunkIndex: 1, updateTime: -1 }) .sort({ chunkIndex: 1, updateTime: -1 })
.skip((pageNum - 1) * pageSize) .skip(offset)
.limit(pageSize) .limit(pageSize)
.lean(), .lean(),
MongoDatasetData.countDocuments(match) MongoDatasetData.countDocuments(match)
]); ]);
return { return {
pageNum, list,
pageSize,
data,
total total
}; };
} }

View File

@ -6,6 +6,7 @@ import { ReadPermissionVal } from '@fastgpt/global/support/permission/constant';
import { ApiRequestProps } from '@fastgpt/service/type/next'; import { ApiRequestProps } from '@fastgpt/service/type/next';
import { DatasetDataListItemType } from '@/global/core/dataset/type'; import { DatasetDataListItemType } from '@/global/core/dataset/type';
import { PaginationProps, PaginationResponse } from '@fastgpt/web/common/fetch/type'; import { PaginationProps, PaginationResponse } from '@fastgpt/web/common/fetch/type';
import { parsePaginationRequest } from '@fastgpt/service/common/api/pagination';
export type GetDatasetDataListProps = PaginationProps & { export type GetDatasetDataListProps = PaginationProps & {
searchText?: string; searchText?: string;
@ -16,7 +17,8 @@ export type GetDatasetDataListRes = PaginationResponse<DatasetDataListItemType>;
async function handler( async function handler(
req: ApiRequestProps<GetDatasetDataListProps> req: ApiRequestProps<GetDatasetDataListProps>
): Promise<GetDatasetDataListRes> { ): Promise<GetDatasetDataListRes> {
let { offset, pageSize = 10, searchText = '', collectionId } = req.body; let { searchText = '', collectionId } = req.body;
let { offset, pageSize } = parsePaginationRequest(req);
pageSize = Math.min(pageSize, 30); pageSize = Math.min(pageSize, 30);

View File

@ -1,8 +1,6 @@
import type { DatasetListItemType } from '@fastgpt/global/core/dataset/type.d';
import { DatasetTypeEnum } from '@fastgpt/global/core/dataset/constants'; import { DatasetTypeEnum } from '@fastgpt/global/core/dataset/constants';
import { MongoDataset } from '@fastgpt/service/core/dataset/schema'; import { MongoDataset } from '@fastgpt/service/core/dataset/schema';
import { authUserPer } from '@fastgpt/service/support/permission/user/auth'; import { authUserPer } from '@fastgpt/service/support/permission/user/auth';
import { getVectorModel } from '@fastgpt/service/core/ai/model';
import { NextAPI } from '@/service/middleware/entry'; import { NextAPI } from '@/service/middleware/entry';
import { DatasetPermission } from '@fastgpt/global/support/permission/dataset/controller'; import { DatasetPermission } from '@fastgpt/global/support/permission/dataset/controller';
import { import {
@ -19,6 +17,8 @@ import { replaceRegChars } from '@fastgpt/global/common/string/tools';
import { getGroupsByTmbId } from '@fastgpt/service/support/permission/memberGroup/controllers'; import { getGroupsByTmbId } from '@fastgpt/service/support/permission/memberGroup/controllers';
import { concatPer } from '@fastgpt/service/support/permission/controller'; import { concatPer } from '@fastgpt/service/support/permission/controller';
import { getOrgIdSetWithParentByTmbId } from '@fastgpt/service/support/permission/org/controllers'; import { getOrgIdSetWithParentByTmbId } from '@fastgpt/service/support/permission/org/controllers';
import { addSourceMember } from '@fastgpt/service/support/user/utils';
import { getVectorModel } from '@fastgpt/service/core/ai/model';
export type GetDatasetListBody = { export type GetDatasetListBody = {
parentId: ParentIdType; parentId: ParentIdType;
@ -167,28 +167,24 @@ async function handler(req: ApiRequestProps<GetDatasetListBody>) {
})(); })();
return { return {
...dataset, _id: dataset._id,
avatar: dataset.avatar,
name: dataset.name,
intro: dataset.intro,
type: dataset.type,
vectorModel: getVectorModel(dataset.vectorModel),
inheritPermission: dataset.inheritPermission,
tmbId: dataset.tmbId,
updateTime: dataset.updateTime,
permission: Per, permission: Per,
privateDataset privateDataset
}; };
}) })
.filter((app) => app.permission.hasReadPer); .filter((app) => app.permission.hasReadPer);
const data = formatDatasets.map<DatasetListItemType>((item) => ({ return addSourceMember({
_id: item._id, list: formatDatasets
avatar: item.avatar, });
name: item.name,
intro: item.intro,
type: item.type,
permission: item.permission,
vectorModel: getVectorModel(item.vectorModel),
inheritPermission: item.inheritPermission,
tmbId: item.tmbId,
updateTime: item.updateTime,
private: item.privateDataset
}));
return data;
} }
export default NextAPI(handler); export default NextAPI(handler);

View File

@ -45,7 +45,11 @@ async function handler(
requestOrigin: req.headers.origin, requestOrigin: req.headers.origin,
mode: 'debug', mode: 'debug',
runningAppInfo: { runningAppInfo: {
id: appId, id: app._id,
teamId: app.teamId,
tmbId: app.tmbId
},
runningUserInfo: {
teamId, teamId,
tmbId tmbId
}, },

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