4.8.19-feature (#3636)

* 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

* perf: scroll page code

* perf: list data

* docs:更新用户答疑 (#3576)

* docs: add custom uid docs (#3572)

* fix: pagination bug (#3577)

* 4.8.19 test (#3584)

* faet: dataset search filter

* fix: scroll page

* fix: collection list api old version (#3591)

* fix: collection list api format

* fix: type error of addSourceMemeber

* fix: scroll fetch (#3592)

* fix: yuque dataset file folder can enter (#3593)

* 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

* fix: api dataset reference tag preview (#3600)

* perf: doc

* feat: chat page config

* fix: http parse (#3634)

* update doc

* fix: http parse

* fix code run node reset template (#3633)

Co-authored-by: Archer <545436317@qq.com>

* docs:faq (#3627)

* docs:faq

* docsFix

* perf: sleep plugin

* fix: selector

---------

Co-authored-by: Finley Ge <32237950+FinleyGe@users.noreply.github.com>
Co-authored-by: Jiangween <145003935+Jiangween@users.noreply.github.com>
Co-authored-by: heheer <heheer@sealos.io>
This commit is contained in:
Archer
2025-01-20 19:42:33 +08:00
committed by GitHub
parent 9f33729ca9
commit 3c97757e4d
170 changed files with 2317 additions and 1615 deletions

View File

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

View File

@@ -33,7 +33,7 @@ export const defaultWhisperConfig: AppWhisperConfigType = {
export const defaultQGConfig: AppQGConfigType = {
open: false,
model: 'gpt-4o-mini',
customPrompt: PROMPT_QUESTION_GUIDE
customPrompt: ''
};
export const defaultChatInputGuideConfig = {

View File

@@ -12,8 +12,9 @@ import { TeamTagSchema as TeamTagsSchemaType } from '@fastgpt/global/support/use
import { StoreEdgeItemType } from '../workflow/type/edge';
import { AppPermission } from '../../support/permission/app/controller';
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 { SourceMemberType } from '../../support/user/type';
export type AppSchema = {
_id: string;
@@ -63,6 +64,7 @@ export type AppListItemType = {
permission: AppPermission;
inheritPermission?: boolean;
private?: boolean;
sourceMember: SourceMemberType;
};
export type AppDetailType = AppSchema & {

View File

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

View File

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

View File

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

View File

@@ -34,7 +34,7 @@ export function getSourceNameIcon({
}
} catch (error) {}
return 'file/fill/manual';
return 'file/fill/file';
}
/* get dataset data default index */

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -12,7 +12,8 @@ const staticPluginList = [
'DingTalkWebhook',
'WeWorkWebhook',
'google',
'bing'
'bing',
'delay'
];
// Run in worker thread (Have npm packages)
const packagePluginList = [
@@ -28,8 +29,7 @@ const packagePluginList = [
'databaseConnection',
'Doc2X',
'Doc2X/PDF2text',
'searchXNG',
'sleep'
'searchXNG'
];
export const list = [...staticPluginList, ...packagePluginList];

View File

@@ -0,0 +1,18 @@
import { delay } from '@fastgpt/global/common/system/utils';
type Props = {
ms: number;
};
type Response = Promise<Number>;
const main = async ({ ms }: Props): Response => {
if (typeof ms !== 'number' || ms <= 0 || ms > 300000) {
return ms;
}
await delay(ms);
return ms;
};
export default main;

View File

@@ -1,11 +1,11 @@
{
"author": "collin",
"version": "4817",
"name": "流程暂停",
"name": "流程等待",
"avatar": "core/workflow/template/sleep",
"intro": "让工作流暂停指定时间后运行",
"intro": "让工作流等待指定时间后运行",
"showStatus": true,
"weight": 10,
"weight": 1,
"isTool": true,
"templateType": "tools",
@@ -20,22 +20,19 @@
"flowNodeType": "pluginInput",
"showStatus": false,
"position": {
"x": 616.4226348688949,
"y": -165.05298493910115
"x": 627.6352390819724,
"y": -165.05298493910118
},
"version": "481",
"inputs": [
{
"renderTypeList": [
"numberInput",
"reference"
],
"renderTypeList": ["numberInput", "reference"],
"selectedTypeIndex": 0,
"valueType": "number",
"canEdit": true,
"key": "ms",
"label": "ms",
"description": "需要暂停的时间,毫秒",
"key": "延迟时长",
"label": "延迟时长",
"description": "需要暂停的时间,单位毫秒",
"defaultValue": 1000,
"list": [
{
@@ -47,8 +44,8 @@
"canSelectFile": true,
"canSelectImg": true,
"required": true,
"toolDescription": "需要暂停的时间,毫秒",
"max": 999999999999,
"toolDescription": "需要暂停的时间,单位毫秒",
"max": 300000,
"min": 1
}
],
@@ -56,8 +53,8 @@
{
"id": "ms",
"valueType": "number",
"key": "ms",
"label": "ms",
"key": "延迟时长",
"label": "延迟时长",
"type": "hidden"
}
]
@@ -70,15 +67,13 @@
"flowNodeType": "pluginOutput",
"showStatus": false,
"position": {
"x": 1925.5772573010433,
"y": -131.55298493910115
"x": 1921.839722563351,
"y": -160.05298493910115
},
"version": "481",
"inputs": [
{
"renderTypeList": [
"reference"
],
"renderTypeList": ["reference"],
"valueType": "any",
"canEdit": true,
"key": "result",
@@ -86,10 +81,7 @@
"isToolOutput": true,
"description": "",
"required": true,
"value": [
"zCJC6zw7c14i",
"httpRawResponse"
]
"value": ["zCJC6zw7c14i", "httpRawResponse"]
}
],
"outputs": []
@@ -116,16 +108,14 @@
"flowNodeType": "httpRequest468",
"showStatus": true,
"position": {
"x": 1152.535395637613,
"y": -433.21496011686054
"x": 1154.4041630064592,
"y": -455.0529849391012
},
"version": "481",
"inputs": [
{
"key": "system_addInputParam",
"renderTypeList": [
"addInputParam"
],
"renderTypeList": ["addInputParam"],
"valueType": "dynamic",
"label": "",
"required": false,
@@ -157,9 +147,7 @@
},
{
"key": "system_httpMethod",
"renderTypeList": [
"custom"
],
"renderTypeList": ["custom"],
"valueType": "string",
"label": "",
"value": "POST",
@@ -171,9 +159,7 @@
},
{
"key": "system_httpTimeout",
"renderTypeList": [
"custom"
],
"renderTypeList": ["custom"],
"valueType": "number",
"label": "",
"value": 30,
@@ -187,24 +173,20 @@
},
{
"key": "system_httpReqUrl",
"renderTypeList": [
"hidden"
],
"renderTypeList": ["hidden"],
"valueType": "string",
"label": "",
"description": "common:core.module.input.description.Http Request Url",
"placeholder": "https://api.ai.com/getInventory",
"required": false,
"value": "sleep",
"value": "delay",
"valueDesc": "",
"debugLabel": "",
"toolDescription": ""
},
{
"key": "system_httpHeader",
"renderTypeList": [
"custom"
],
"renderTypeList": ["custom"],
"valueType": "any",
"value": [],
"label": "",
@@ -217,9 +199,7 @@
},
{
"key": "system_httpParams",
"renderTypeList": [
"hidden"
],
"renderTypeList": ["hidden"],
"valueType": "any",
"value": [],
"label": "",
@@ -231,9 +211,7 @@
},
{
"key": "system_httpJsonBody",
"renderTypeList": [
"hidden"
],
"renderTypeList": ["hidden"],
"valueType": "any",
"value": "{\n\"ms\": {{$pluginInput.ms$}}\n}",
"label": "",
@@ -245,9 +223,7 @@
},
{
"key": "system_httpFormBody",
"renderTypeList": [
"hidden"
],
"renderTypeList": ["hidden"],
"valueType": "any",
"value": [],
"label": "",
@@ -259,9 +235,7 @@
},
{
"key": "system_httpContentType",
"renderTypeList": [
"hidden"
],
"renderTypeList": ["hidden"],
"valueType": "string",
"value": "json",
"label": "",
@@ -338,32 +312,7 @@
}
],
"chatConfig": {
"welcomeText": "",
"variables": [],
"questionGuide": {
"open": false,
"model": "gpt-4o-mini",
"customPrompt": "You are an AI assistant tasked with predicting the user's next question based on the conversation history. Your goal is to generate 3 potential questions that will guide the user to continue the conversation. When generating these questions, adhere to the following rules:\n\n1. Use the same language as the user's last question in the conversation history.\n2. Keep each question under 20 characters in length.\n\nAnalyze the conversation history provided to you and use it as context to generate relevant and engaging follow-up questions. Your predictions should be logical extensions of the current topic or related areas that the user might be interested in exploring further.\n\nRemember to maintain consistency in tone and style with the existing conversation while providing diverse options for the user to choose from. Your goal is to keep the conversation flowing naturally and help the user delve deeper into the subject matter or explore related topics."
},
"ttsConfig": {
"type": "web"
},
"whisperConfig": {
"open": false,
"autoSend": false,
"autoTTSResponse": false
},
"chatInputGuide": {
"open": false,
"textList": [],
"customUrl": ""
},
"instruction": "",
"autoExecute": {
"open": false,
"defaultPrompt": ""
},
"_id": "6784ba2c227f92f6c723ead0"
"welcomeText": ""
}
}
}
}

View File

@@ -1,17 +0,0 @@
type Props = {
ms: number;
};
type Response = Promise<{
result: any;
}>;
const main = async ({ ms }: Props): Response => {
await new Promise((resolve) => {
setTimeout(resolve, ms);
});
return {
result: ms
};
};
export default main;

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';
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 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')) {
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 }) => {

View File

@@ -284,7 +284,7 @@ export async function searchDatasetData(props: SearchDatasetDataProps) {
{
_id: { $in: collectionIdList }
},
'_id name fileId rawLink externalFileId externalFileUrl',
'_id name fileId rawLink apiFileId externalFileId externalFileUrl',
{ ...readFromSecondary }
).lean()
]);
@@ -525,7 +525,7 @@ export async function searchDatasetData(props: SearchDatasetDataProps) {
{
_id: { $in: searchResults.map((item) => item.collectionId) }
},
'_id name fileId rawLink externalFileId externalFileUrl',
'_id name fileId rawLink apiFileId externalFileId externalFileUrl',
{ ...readFromSecondary }
).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 { MongoDataset } from '../../../dataset/schema';
import { i18nT } from '../../../../../web/i18n/utils';
import { filterDatasetsByTmbId } from '../../../dataset/utils';
type DatasetSearchProps = ModuleDispatchProps<{
[NodeInputKeyEnum.datasetSelectList]: SelectedDatasetType;
@@ -29,6 +30,7 @@ type DatasetSearchProps = ModuleDispatchProps<{
[NodeInputKeyEnum.datasetSearchExtensionModel]: string;
[NodeInputKeyEnum.datasetSearchExtensionBg]: string;
[NodeInputKeyEnum.collectionFilterMatch]: string;
[NodeInputKeyEnum.authTmbId]: boolean;
}>;
export type DatasetSearchResponse = DispatchNodeResultType<{
[NodeOutputKeyEnum.datasetQuoteQA]: SearchDataResponseItemType[];
@@ -39,6 +41,7 @@ export async function dispatchDatasetSearch(
): Promise<DatasetSearchResponse> {
const {
runningAppInfo: { teamId },
runningUserInfo: { tmbId },
histories,
node,
params: {
@@ -52,7 +55,8 @@ export async function dispatchDatasetSearch(
datasetSearchUsingExtensionQuery,
datasetSearchExtensionModel,
datasetSearchExtensionBg,
collectionFilterMatch
collectionFilterMatch,
authTmbId = false
}
} = props as DatasetSearchProps;
@@ -64,18 +68,20 @@ export async function dispatchDatasetSearch(
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) {
return {
quoteQA: [],
[DispatchNodeResponseKeyEnum.nodeResponse]: {
totalPoints: 0,
query: '',
limit,
searchMode
},
nodeDispatchUsages: [],
[DispatchNodeResponseKeyEnum.toolResponses]: []
};
return emptyResult;
}
// query extension
@@ -83,13 +89,24 @@ export async function dispatchDatasetSearch(
? getLLMModel(datasetSearchExtensionModel)
: undefined;
const { concatQueries, rewriteQuery, aiExtensionResult } = await datasetSearchQueryExtension({
query: userChatInput,
extensionModel,
extensionBg: datasetSearchExtensionBg,
histories: getHistories(6, histories)
});
const [{ concatQueries, rewriteQuery, aiExtensionResult }, datasetIds] = await Promise.all([
datasetSearchQueryExtension({
query: userChatInput,
extensionModel,
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);
// get vector
@@ -110,7 +127,7 @@ export async function dispatchDatasetSearch(
model: vectorModel.model,
similarity,
limit,
datasetIds: datasets.map((item) => item.datasetId),
datasetIds,
searchMode,
usingReRank: usingReRank && (await checkTeamReRankPermission(teamId)),
collectionFilterMatch

View File

@@ -72,6 +72,7 @@ export const dispatchRunPlugin = async (props: RunPluginProps): Promise<RunPlugi
showStatus: false
};
});
const runtimeVariables = {
...filterSystemVariables(props.variables),
appId: String(plugin.id)

View File

@@ -127,8 +127,16 @@ export const dispatchHttp468Request = async (props: HttpRequestProps): Promise<H
if (typeof val === 'object') return JSON.stringify(val);
if (typeof val === 'string') {
const str = JSON.stringify(val);
return str.startsWith('"') && str.endsWith('"') ? str.slice(1, -1) : str;
try {
const parsed = JSON.parse(val);
if (typeof parsed === 'object') {
return JSON.stringify(parsed);
}
return val;
} catch (error) {
const str = JSON.stringify(val);
return str.startsWith('"') && str.endsWith('"') ? str.slice(1, -1) : str;
}
}
return String(val);
@@ -235,7 +243,9 @@ export const dispatchHttp468Request = async (props: HttpRequestProps): Promise<H
}
if (!httpJsonBody) return {};
if (httpContentType === ContentTypes.json) {
return json5.parse(replaceJsonBodyString(httpJsonBody));
httpJsonBody = replaceJsonBodyString(httpJsonBody);
console.log(httpJsonBody);
return json5.parse(httpJsonBody);
}
// Raw text, xml

View File

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

View File

@@ -47,14 +47,11 @@ export const OrgSchema = new Schema(
OrgSchema.virtual('members', {
ref: OrgMemberCollectionName,
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 {
OrgSchema.index({

View File

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

View File

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

View File

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

View File

@@ -7,6 +7,7 @@ import {
TeamMemberCollectionName,
TeamCollectionName
} from '@fastgpt/global/support/user/team/constant';
import { getRandomUserAvatar } from '@fastgpt/global/support/user/utils';
const TeamMemberSchema = new Schema({
teamId: {
@@ -19,6 +20,10 @@ const TeamMemberSchema = new Schema({
ref: userCollectionName,
required: true
},
avatar: {
type: String,
default: () => getRandomUserAvatar()
},
name: {
type: String,
default: 'Member'
@@ -39,7 +44,6 @@ const TeamMemberSchema = new Schema({
// Abandoned
role: {
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 { MongoTeamMember } from './team/teamMemberSchema';
import { ClientSession } from '../../common/mongo';
/* export dataset limit */
export const updateExportDatasetLimit = async (teamId: string) => {
@@ -67,3 +70,41 @@ export const checkWebSyncLimit = async ({
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 & {
offset: number;
pageSize: number;
};
export type PaginationResponse<T = any> = {
import { RequireOnlyOne } from '@fastgpt/global/common/type/utils';
type PaginationProps<T = {}> = T & {
pageSize: number | string;
} & RequireOnlyOne<{
offset: number | string;
pageNum: number | string;
}>;
type PaginationResponse<T = {}> = {
total: number;
list: T[];
};

View File

@@ -23,7 +23,7 @@ function AvatarGroup({
<Flex position="relative">
{avatars.slice(0, max).map((avatar, index) => (
<Avatar
key={avatar + groupId}
key={index}
src={avatar}
position={index > 0 ? 'absolute' : 'relative'}
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 { useRequest2 } from '../../../hooks/useRequest';
import MyDivider from '../MyDivider';
import { useScrollPagination } from '../../../hooks/useScrollPagination';
/** 选择组件 Props 类型
* value: 选中的值
* placeholder: 占位符
* list: 列表数据
* isLoading: 是否加载中
* ScrollData: 分页滚动数据控制器 [useScrollPagination] 的返回值
* */
export type SelectProps<T = any> = ButtonProps & {
value?: T;
placeholder?: string;
@@ -34,6 +42,7 @@ export type SelectProps<T = any> = ButtonProps & {
}[];
isLoading?: boolean;
onchange?: (val: T) => any | Promise<any>;
ScrollData?: ReturnType<typeof useScrollPagination>['ScrollData'];
};
const MySelect = <T = any,>(
@@ -44,6 +53,7 @@ const MySelect = <T = any,>(
list = [],
onchange,
isLoading = false,
ScrollData,
...props
}: SelectProps<T>,
ref: ForwardedRef<{
@@ -87,6 +97,46 @@ const MySelect = <T = any,>(
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>
))}
</>
);
}, [list, value]);
return (
<Box
css={css({
@@ -154,39 +204,7 @@ const MySelect = <T = any,>(
maxH={'40vh'}
overflowY={'auto'}
>
{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>
))}
{ScrollData ? <ScrollData>{ListRender}</ScrollData> : ListRender}
</MenuList>
</Menu>
</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 { useToast } from './useToast';
import { getErrText } from '@fastgpt/global/common/error/utils';
import {
useBoolean,
useLockFn,
@@ -14,37 +13,33 @@ import {
useThrottleEffect
} from 'ahooks';
import { PaginationProps, PaginationResponse } from '../common/fetch/type';
const thresholdVal = 200;
type PagingData<T> = {
pageNum: number;
pageSize: number;
data: T[];
total?: number;
};
export function usePagination<ResT = any>({
api,
pageSize = 10,
params = {},
defaultRequest = true,
type = 'button',
onChange,
refreshDeps,
scrollLoadType = 'bottom',
EmptyTip
}: {
api: (data: any) => Promise<PagingData<ResT>>;
pageSize?: number;
params?: Record<string, any>;
defaultRequest?: boolean;
type?: 'button' | 'scroll';
onChange?: (pageNum: number) => void;
refreshDeps?: any[];
throttleWait?: number;
scrollLoadType?: 'top' | 'bottom';
EmptyTip?: React.JSX.Element;
}) {
export function usePagination<DataT, ResT = {}>(
api: (data: PaginationProps<DataT>) => Promise<PaginationResponse<ResT>>,
{
pageSize = 10,
params,
defaultRequest = true,
type = 'button',
onChange,
refreshDeps,
scrollLoadType = 'bottom',
EmptyTip
}: {
pageSize?: number;
params?: DataT;
defaultRequest?: boolean;
type?: 'button' | 'scroll';
onChange?: (pageNum: number) => void;
refreshDeps?: any[];
throttleWait?: number;
scrollLoadType?: 'top' | 'bottom';
EmptyTip?: React.JSX.Element;
}
) {
const { toast } = useToast();
const { t } = useTranslation();
@@ -64,7 +59,7 @@ export function usePagination<ResT = any>({
setTrue();
try {
const res: PagingData<ResT> = await api({
const res = await api({
pageNum: num,
pageSize,
...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();
} else {
setData((prevData) => (num === 1 ? res.data : [...prevData, ...res.data]));
setData((prevData) => (num === 1 ? res.list : [...prevData, ...res.list]));
}
} else {
setData(res.data);
setData(res.list);
}
onChange?.(num);

View File

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

View File

@@ -39,6 +39,7 @@
"classification": "Classification",
"click_to_resume": "Click to Resume",
"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_owner": "Unauthorized Application Owner",
"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.token_error_code.403": "Invalid Login Status, Please Re-login",
"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.un_auth_user": "User Not Found",
"common.Action": "Action",
@@ -1273,6 +1273,7 @@
"user.team.role.Visitor": "visitor",
"user.team.role.writer": "writable member",
"user.type": "Type",
"user_leaved": "Leaved",
"verification": "Verification",
"workflow.template.communication": "Communication",
"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",
"application_call": "Application Call",
"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.",
"choose_another_application_to_call": "Select another application to call",
"classification_result": "Classification Result",

View File

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

View File

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

View File

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

View File

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

View File

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