Repository: labring/FastGPT
Pull request #3979: feat: invitation link
Author: login: FinleyGe
Labels: name: size/XL
Created: 2025-03-05T04:10:01Z | Merged: 2025-03-12T05:47:15Z

- **feat: invitation link schema and apis**
- **feat: add invitation link**
- **feat: member status: active, leave, forbidden**



Diff:
diff --git a/packages/global/common/error/code/team.ts b/packages/global/common/error/code/team.ts
index 7c2ab6790897..f60d08ccad43 100644
--- a/packages/global/common/error/code/team.ts
+++ b/packages/global/common/error/code/team.ts
@@ -24,7 +24,10 @@ export enum TeamErrEnum {
   cannotModifyRootOrg = 'cannotModifyRootOrg',
   cannotDeleteNonEmptyOrg = 'cannotDeleteNonEmptyOrg',
   cannotDeleteDefaultGroup = 'cannotDeleteDefaultGroup',
-  userNotActive = 'userNotActive'
+  userNotActive = 'userNotActive',
+  invitationLinkInvalid = 'invitationLinkInvalid',
+  youHaveBeenInTheTeam = 'youHaveBeenInTheTeam',
+  tooManyInvitations = 'tooManyInvitations'
 }
 
 const teamErr = [
@@ -112,6 +115,18 @@ const teamErr = [
   {
     statusText: TeamErrEnum.cannotDeleteNonEmptyOrg,
     message: i18nT('common:code_error.team_error.cannot_delete_non_empty_org')
+  },
+  {
+    statusText: TeamErrEnum.invitationLinkInvalid,
+    message: i18nT('common:code_error.team_error.invitation_link_invalid')
+  },
+  {
+    statusText: TeamErrEnum.youHaveBeenInTheTeam,
+    message: i18nT('common:code_error.team_error.you_have_been_in_the_team')
+  },
+  {
+    statusText: TeamErrEnum.tooManyInvitations,
+    message: i18nT('common:code_error.team_error.too_many_invitations')
   }
 ];
 
diff --git a/packages/global/support/user/team/constant.ts b/packages/global/support/user/team/constant.ts
index ea40229bd6e5..f596cfcd4ed9 100644
--- a/packages/global/support/user/team/constant.ts
+++ b/packages/global/support/user/team/constant.ts
@@ -14,29 +14,28 @@ export const TeamMemberRoleMap = {
 };
 
 export enum TeamMemberStatusEnum {
-  waiting = 'waiting',
   active = 'active',
-  reject = 'reject',
-  leave = 'leave'
+  leave = 'leave',
+  forbidden = 'forbidden'
 }
 
 export const TeamMemberStatusMap = {
-  [TeamMemberStatusEnum.waiting]: {
-    label: 'user.team.member.waiting',
-    color: 'orange.600'
-  },
   [TeamMemberStatusEnum.active]: {
     label: 'user.team.member.active',
     color: 'green.600'
   },
-  [TeamMemberStatusEnum.reject]: {
-    label: 'user.team.member.reject',
-    color: 'red.600'
-  },
   [TeamMemberStatusEnum.leave]: {
     label: 'user.team.member.leave',
     color: 'red.600'
+  },
+  [TeamMemberStatusEnum.forbidden]: {
+    label: 'user.team.member.forbidden',
+    color: 'red.600'
   }
 };
 
-export const notLeaveStatus = { $ne: TeamMemberStatusEnum.leave };
+export const notLeaveStatus = {
+  $not: {
+    $in: [TeamMemberStatusEnum.leave, TeamMemberStatusEnum.forbidden]
+  }
+};
diff --git a/packages/global/support/user/team/controller.d.ts b/packages/global/support/user/team/controller.d.ts
index cf17b7addbbd..8d25986c036e 100644
--- a/packages/global/support/user/team/controller.d.ts
+++ b/packages/global/support/user/team/controller.d.ts
@@ -40,11 +40,6 @@ export type UpdateInviteProps = {
   status: TeamMemberSchema['status'];
 };
 
-export type UpdateStatusProps = {
-  tmbId: string;
-  status: TeamMemberSchema['status'];
-};
-
 export type InviteMemberResponse = Record<
   'invite' | 'inValid' | 'inTeam',
   { username: string; userId: string }[]
diff --git a/packages/service/support/user/team/invitationLink/constants.ts b/packages/service/support/user/team/invitationLink/constants.ts
new file mode 100644
index 000000000000..bf5427449596
--- /dev/null
+++ b/packages/service/support/user/team/invitationLink/constants.ts
@@ -0,0 +1 @@
+export const MaxInvitationLinksAmount = 10;
diff --git a/packages/service/support/user/team/invitationLink/controllers.ts b/packages/service/support/user/team/invitationLink/controllers.ts
new file mode 100644
index 000000000000..4eecab463f93
--- /dev/null
+++ b/packages/service/support/user/team/invitationLink/controllers.ts
@@ -0,0 +1,3 @@
+export function isForbidden({ expires, forbidden }: { expires: Date; forbidden?: boolean }) {
+  return forbidden || new Date(expires) < new Date();
+}
diff --git a/packages/service/support/user/team/invitationLink/schema.ts b/packages/service/support/user/team/invitationLink/schema.ts
new file mode 100644
index 000000000000..71a1b6721006
--- /dev/null
+++ b/packages/service/support/user/team/invitationLink/schema.ts
@@ -0,0 +1,54 @@
+import {
+  TeamCollectionName,
+  TeamMemberCollectionName
+} from '@fastgpt/global/support/user/team/constant';
+import { connectionMongo, getMongoModel } from '../../../../common/mongo';
+import { InvitationSchemaType } from './type';
+import addDays from 'date-fns/esm/fp/addDays/index.js';
+const { Schema } = connectionMongo;
+
+export const InvitationCollectionName = 'team_invitation_links';
+
+const InvitationSchema = new Schema({
+  teamId: {
+    type: Schema.Types.ObjectId,
+    ref: TeamCollectionName,
+    required: true
+  },
+  usedTimesLimit: {
+    type: Number
+  },
+  forbidden: {
+    type: Boolean
+  },
+  expires: {
+    type: Date
+  },
+  description: {
+    type: String
+  },
+  members: {
+    type: [String],
+    default: []
+  }
+});
+
+InvitationSchema.virtual('team', {
+  ref: TeamCollectionName,
+  localField: 'teamId',
+  foreignField: '_id',
+  justOne: true
+});
+
+InvitationSchema.index({ expires: 1 }, { expireAfterSeconds: 30 * 24 * 60 * 60 });
+
+try {
+  InvitationSchema.index({ teamId: 1 }, { background: true });
+} catch (error) {
+  console.log(error);
+}
+
+export const MongoInvitationLink = getMongoModel<InvitationSchemaType>(
+  InvitationCollectionName,
+  InvitationSchema
+);
diff --git a/packages/service/support/user/team/invitationLink/type.ts b/packages/service/support/user/team/invitationLink/type.ts
new file mode 100644
index 000000000000..0338273789ef
--- /dev/null
+++ b/packages/service/support/user/team/invitationLink/type.ts
@@ -0,0 +1,37 @@
+import { TeamMemberSchema } from '@fastgpt/global/support/user/team/type';
+
+export type InvitationSchemaType = {
+  _id: string;
+  teamId: string;
+  usedTimesLimit?: number;
+  forbidden?: boolean;
+  expires: Date;
+  description: string;
+  members: string[];
+};
+
+export type InvitationType = Omit<InvitationSchemaType, 'members'> & {
+  members: {
+    tmbId: string;
+    avatar: string;
+    name: string;
+  }[];
+};
+
+export type InvitationLinkExpiresType = '30m' | '7d' | '1y';
+
+export type InvitationLinkCreateType = {
+  description: string;
+  expires: InvitationLinkExpiresType;
+  usedTimesLimit: number;
+};
+export type InvitationLinkUpdateType = Partial<
+  Omit<InvitationSchemaType, 'members' | 'teamId' | '_id'>
+> & {
+  linkId: string;
+};
+
+export type InvitationInfoType = InvitationSchemaType & {
+  teamAvatar: string;
+  teamName: string;
+};
diff --git a/packages/web/components/common/Avatar/AvatarGroup.tsx b/packages/web/components/common/Avatar/AvatarGroup.tsx
index 199373f05a3f..6f9f3ce03b59 100644
--- a/packages/web/components/common/Avatar/AvatarGroup.tsx
+++ b/packages/web/components/common/Avatar/AvatarGroup.tsx
@@ -10,15 +10,7 @@ import { Box, Flex } from '@chakra-ui/react';
  * @param [groupId] - group id to make the key unique
  * @returns
  */
-function AvatarGroup({
-  avatars,
-  max = 3,
-  groupId
-}: {
-  max?: number;
-  avatars: string[];
-  groupId?: string;
-}) {
+function AvatarGroup({ avatars, max = 3 }: { max?: number; avatars: string[] }) {
   return (
     <Flex position="relative">
       {avatars.slice(0, max).map((avatar, index) => (
diff --git a/packages/web/components/common/Icon/constants.ts b/packages/web/components/common/Icon/constants.ts
index 1a91861de9ab..430a26490b00 100644
--- a/packages/web/components/common/Icon/constants.ts
+++ b/packages/web/components/common/Icon/constants.ts
@@ -61,6 +61,8 @@ export const iconPaths = {
   'common/leftArrowLight': () => import('./icons/common/leftArrowLight.svg'),
   'common/line': () => import('./icons/common/line.svg'),
   'common/lineChange': () => import('./icons/common/lineChange.svg'),
+  'common/lineStop': () => import('./icons/common/lineStop.svg'),
+  'common/link': () => import('./icons/common/link.svg'),
   'common/linkBlue': () => import('./icons/common/linkBlue.svg'),
   'common/list': () => import('./icons/common/list.svg'),
   'common/loading': () => import('./icons/common/loading.svg'),
diff --git a/packages/web/components/common/Icon/icons/common/lineStop.svg b/packages/web/components/common/Icon/icons/common/lineStop.svg
new file mode 100644
index 000000000000..b3f4c143dc11
--- /dev/null
+++ b/packages/web/components/common/Icon/icons/common/lineStop.svg
@@ -0,0 +1,10 @@
+<svg width="17" height="16" viewBox="0 0 17 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<g clip-path="url(#clip0_17994_4)">
+<path fill-rule="evenodd" clip-rule="evenodd" d="M8.98584 2.23348C5.80108 2.23348 3.21932 4.81524 3.21932 8C3.21932 11.1848 5.80108 13.7665 8.98584 13.7665C12.1706 13.7665 14.7524 11.1848 14.7524 8C14.7524 4.81524 12.1706 2.23348 8.98584 2.23348ZM1.93787 8C1.93787 4.10751 5.09335 0.952026 8.98584 0.952026C12.8783 0.952026 16.0338 4.10751 16.0338 8C16.0338 11.8925 12.8783 15.048 8.98584 15.048C5.09335 15.048 1.93787 11.8925 1.93787 8ZM6.42294 6.07782C6.42294 5.72396 6.7098 5.4371 7.06366 5.4371H10.908C11.2619 5.4371 11.5487 5.72396 11.5487 6.07782V9.92217C11.5487 10.276 11.2619 10.5629 10.908 10.5629H7.06366C6.7098 10.5629 6.42294 10.276 6.42294 9.92217V6.07782ZM7.70439 6.71855V9.28145H10.2673V6.71855H7.70439Z" fill="#485264"/>
+</g>
+<defs>
+<clipPath id="clip0_17994_4">
+<rect width="16" height="16" fill="white" transform="translate(0.98584)"/>
+</clipPath>
+</defs>
+</svg>
diff --git a/packages/web/components/common/Icon/icons/common/link.svg b/packages/web/components/common/Icon/icons/common/link.svg
new file mode 100644
index 000000000000..9855cd94a8a3
--- /dev/null
+++ b/packages/web/components/common/Icon/icons/common/link.svg
@@ -0,0 +1,3 @@
+<svg width="17" height="16" viewBox="0 0 17 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path fill-rule="evenodd" clip-rule="evenodd" d="M8.98608 2.34313C10.5482 0.78103 13.0808 0.78103 14.6429 2.34313C16.205 3.90522 16.205 6.43788 14.6429 7.99998L13.7001 8.94279C13.4398 9.20314 13.0177 9.20314 12.7573 8.94279C12.497 8.68244 12.497 8.26033 12.7573 7.99998L13.7001 7.05717C14.7415 6.01577 14.7415 4.32733 13.7001 3.28594C12.6587 2.24454 10.9703 2.24454 9.92889 3.28594L8.98608 4.22875C8.72573 4.48909 8.30362 4.48909 8.04327 4.22875C7.78292 3.9684 7.78292 3.54629 8.04327 3.28594L8.98608 2.34313ZM11.7908 5.19523C12.0512 5.45558 12.0512 5.87769 11.7908 6.13804L7.12415 10.8047C6.8638 11.0651 6.44169 11.0651 6.18134 10.8047C5.92099 10.5444 5.92099 10.1222 6.18134 9.8619L10.848 5.19523C11.1084 4.93488 11.5305 4.93488 11.7908 5.19523ZM5.21484 7.05717C5.47519 7.31752 5.47519 7.73963 5.21484 7.99998L4.27204 8.94279C3.23064 9.98419 3.23064 11.6726 4.27204 12.714C5.31343 13.7554 7.00187 13.7554 8.04327 12.714L8.98608 11.7712C9.24643 11.5109 9.66854 11.5109 9.92889 11.7712C10.1892 12.0316 10.1892 12.4537 9.92889 12.714L8.98608 13.6568C7.42398 15.2189 4.89132 15.2189 3.32923 13.6568C1.76713 12.0947 1.76713 9.56208 3.32923 7.99998L4.27204 7.05717C4.53239 6.79682 4.9545 6.79682 5.21484 7.05717Z" fill="#485264"/>
+</svg>
diff --git a/packages/web/i18n/en/account_team.json b/packages/web/i18n/en/account_team.json
index 8641c3c8f921..e4c6d90b411a 100644
--- a/packages/web/i18n/en/account_team.json
+++ b/packages/web/i18n/en/account_team.json
@@ -35,5 +35,27 @@
   "user_team_invite_member": "Invite members",
   "user_team_leave_team": "Leave the team",
   "user_team_leave_team_failed": "Failure to leave the team",
-  "waiting": "To be accepted"
+  "waiting": "To be accepted",
+  "invitation_link_list": "Invitation link list",
+  "create_invitation_link": "Create Invitation Link",
+  "invitation_link_description": "Link description",
+  "30mins": "30 Minutes",
+  "7days": "7 Days",
+  "1year": "1 Year",
+  "unlimited": "Unlimited",
+  "1person": "1 person",
+  "expires": "Expiration",
+  "used_times_limit": "Limit",
+  "invited": "Invited",
+  "has_forbidden": "Forbidden",
+  "forbidden": "Forbidden",
+  "copy_link": "Copy link",
+  "handle_invitation": "Handle Invitation",
+  "ignore": "Ignore",
+  "forbid_success": "Forbid success",
+  "forbid_hint": "After forbidden, this invitation link will become invalid. This action is irreversible. Are you sure you want to deactivate?",
+  "confirm_forbidden": "Confirm forbidden",
+  "invitation_link_auto_clean_hint": "Expired links will be automatically cleaned up after 30 days",
+  "has_invited": "Invited",
+  "invitation_link_has_been_invalid": "The invitation link has expired. Please contact the team administrator"
 }
diff --git a/packages/web/i18n/en/common.json b/packages/web/i18n/en/common.json
index d42335a013d8..18c8b4559b32 100644
--- a/packages/web/i18n/en/common.json
+++ b/packages/web/i18n/en/common.json
@@ -100,6 +100,9 @@
   "code_error.team_error.un_auth": "Unauthorized to Operate This Team",
   "code_error.team_error.user_not_active": "The user did not accept or has left the team",
   "code_error.team_error.website_sync_not_enough": "The free version cannot be synchronized with the web site ~",
+  "code_error.team_error.invitation_link_invalid": "Invitation link is invalid",
+  "code_error.team_error.you_have_been_in_the_team": "You are already in this team",
+  "code_error.team_error.too_many_invitations": "You have reached the maximum number of active invitation links, please clean up some links first",
   "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.bin_visitor_guest": "You Are Currently a Guest, Unauthorized to Operate",
diff --git a/packages/web/i18n/zh-CN/account_team.json b/packages/web/i18n/zh-CN/account_team.json
index d0fa3eb6df59..fd70a1b74a28 100644
--- a/packages/web/i18n/zh-CN/account_team.json
+++ b/packages/web/i18n/zh-CN/account_team.json
@@ -51,5 +51,27 @@
   "confirm_delete_from_org": "确认将 {{username}} 移出部门？",
   "search_org": "搜索部门",
   "notification_recieve": "团队通知接收",
-  "set_name_avatar": "团队头像 & 团队名"
+  "set_name_avatar": "团队头像 & 团队名",
+  "invitation_link_list": "链接列表",
+  "create_invitation_link": "创建邀请链接",
+  "invitation_link_description": "链接描述",
+  "30mins": "30分钟",
+  "7days": "7天",
+  "1year": "1年",
+  "unlimited": "无限制",
+  "1person": "1人",
+  "expires": "有效期",
+  "used_times_limit": "有效人数",
+  "invited": "已邀请",
+  "has_forbidden": "已失效",
+  "forbidden": "停用",
+  "copy_link": "复制链接",
+  "handle_invitation": "处理团队邀请",
+  "ignore": "忽略",
+  "forbid_success": "停用成功",
+  "forbid_hint": "停用后，该邀请链接将失效。 该操作不可撤销，是否确认停用？",
+  "confirm_forbidden": "确认停用",
+  "invitation_link_auto_clean_hint": "已失效链接将在30天后自动清理",
+  "has_invited": "已邀请",
+  "invitation_link_has_been_invalid": "邀请链接已失效，请联系团队管理员"
 }
diff --git a/packages/web/i18n/zh-CN/common.json b/packages/web/i18n/zh-CN/common.json
index 5317f35f0361..6d95d9406967 100644
--- a/packages/web/i18n/zh-CN/common.json
+++ b/packages/web/i18n/zh-CN/common.json
@@ -104,6 +104,9 @@
   "code_error.team_error.un_auth": "无权操作该团队",
   "code_error.team_error.user_not_active": "用户未接受或已离开团队",
   "code_error.team_error.website_sync_not_enough": "免费版无法使用Web站点同步~",
+  "code_error.team_error.invitation_link_invalid": "邀请链接无效",
+  "code_error.team_error.you_have_been_in_the_team": "你已经在该团队中",
+  "code_error.team_error.too_many_invitations": "您的有效邀请链接数已达上限，请先清理链接",
   "code_error.token_error_code.403": "登录状态无效，请重新登录",
   "code_error.user_error.balance_not_enough": "账号余额不足~",
   "code_error.user_error.bin_visitor_guest": "您当前身份为游客，无权操作",
diff --git a/packages/web/i18n/zh-Hant/account_team.json b/packages/web/i18n/zh-Hant/account_team.json
index 94a21818b3f5..7e952eb450b3 100644
--- a/packages/web/i18n/zh-Hant/account_team.json
+++ b/packages/web/i18n/zh-Hant/account_team.json
@@ -35,5 +35,27 @@
   "user_team_invite_member": "邀請成員",
   "user_team_leave_team": "離開團隊",
   "user_team_leave_team_failed": "離開團隊失敗",
-  "waiting": "待接受"
+  "waiting": "待接受",
+  "invitation_link_list": "連結列表",
+  "create_invitation_link": "建立邀請連結",
+  "invitation_link_description": "連結描述",
+  "30mins": "30分鐘",
+  "7days": "7天",
+  "1year": "1年",
+  "unlimited": "無限制",
+  "1person": "1人",
+  "expires": "有效期",
+  "used_times_limit": "有效人數",
+  "invited": "已邀請",
+  "has_forbidden": "已失效",
+  "forbidden": "停用",
+  "copy_link": "複製連結",
+  "handle_invitation": "處理團隊邀請",
+  "ignore": "忽略",
+  "forbid_success": "停用成功",
+  "forbid_hint": "停用後，該邀請連結將失效。 該操作不可撤銷，是否確認停用？",
+  "confirm_forbidden": "確認停用",
+  "invitation_link_auto_clean_hint": "已失效連結將在30天後自動清理",
+  "has_invited": "已邀請",
+  "invitation_link_has_been_invalid": "邀請連結已失效，請聯繫團隊管理員"
 }
diff --git a/packages/web/i18n/zh-Hant/common.json b/packages/web/i18n/zh-Hant/common.json
index 63da238a91ec..c21a19d2535d 100644
--- a/packages/web/i18n/zh-Hant/common.json
+++ b/packages/web/i18n/zh-Hant/common.json
@@ -99,6 +99,9 @@
   "code_error.team_error.un_auth": "無權操作此團隊",
   "code_error.team_error.user_not_active": "使用者未接受或已離開團隊",
   "code_error.team_error.website_sync_not_enough": "免費版無法使用Web站點同步~",
+  "code_error.team_error.invitation_link_invalid": "邀請連結無效",
+  "code_error.team_error.you_have_been_in_the_team": "你已經在該團隊中",
+  "code_error.team_error.too_many_invitations": "您的有效邀請連結數已達上限，請先清理連結",
   "code_error.token_error_code.403": "登入狀態無效，請重新登入",
   "code_error.user_error.balance_not_enough": "帳戶餘額不足",
   "code_error.user_error.bin_visitor_guest": "您目前身份為訪客，無權操作",
diff --git a/projects/app/src/components/Layout/index.tsx b/projects/app/src/components/Layout/index.tsx
index 2ece77c2791a..0be42f608be3 100644
--- a/projects/app/src/components/Layout/index.tsx
+++ b/projects/app/src/components/Layout/index.tsx
@@ -18,7 +18,6 @@ import WorkorderButton from './WorkorderButton';
 
 const Navbar = dynamic(() => import('./navbar'));
 const NavbarPhone = dynamic(() => import('./navbarPhone'));
-const UpdateInviteModal = dynamic(() => import('@/components/support/user/team/UpdateInviteModal'));
 const NotSufficientModal = dynamic(() => import('@/components/support/wallet/NotSufficientModal'));
 const SystemMsgModal = dynamic(() => import('@/components/support/user/inform/SystemMsgModal'));
 const ImportantInform = dynamic(() => import('@/components/support/user/inform/ImportantInform'));
@@ -151,7 +150,6 @@ const Layout = ({ children }: { children: JSX.Element }) => {
       </Box>
       {feConfigs?.isPlus && (
         <>
-          {!!userInfo && <UpdateInviteModal />}
           {notSufficientModalType && <NotSufficientModal type={notSufficientModalType} />}
           {!!userInfo && <SystemMsgModal />}
           {showUpdateNotification && (
diff --git a/projects/app/src/components/support/user/team/UpdateInviteModal/index.tsx b/projects/app/src/components/support/user/team/UpdateInviteModal/index.tsx
deleted file mode 100644
index a710da4e12a2..000000000000
--- a/projects/app/src/components/support/user/team/UpdateInviteModal/index.tsx
+++ /dev/null
@@ -1,132 +0,0 @@
-import React from 'react';
-import { useTranslation } from 'next-i18next';
-import MyModal from '@fastgpt/web/components/common/MyModal';
-import { Button, ModalFooter, ModalBody, Flex, Box, useTheme } from '@chakra-ui/react';
-import { getTeamList, updateInviteResult } from '@/web/support/user/team/api';
-import { TeamMemberStatusEnum } from '@fastgpt/global/support/user/team/constant';
-import Avatar from '@fastgpt/web/components/common/Avatar';
-import { useToast } from '@fastgpt/web/hooks/useToast';
-import { useConfirm } from '@fastgpt/web/hooks/useConfirm';
-import { useSystemStore } from '@/web/common/system/useSystemStore';
-import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
-import { useUserStore } from '@/web/support/user/useUserStore';
-
-const UpdateInviteModal = () => {
-  const { t } = useTranslation();
-  const theme = useTheme();
-  const { toast } = useToast();
-  const { feConfigs } = useSystemStore();
-  const { initUserInfo } = useUserStore();
-
-  const { ConfirmModal, openConfirm } = useConfirm({});
-
-  const { data: inviteList = [], run: fetchInviteList } = useRequest2(
-    async () => (feConfigs.isPlus ? getTeamList(TeamMemberStatusEnum.waiting) : []),
-    {
-      manual: false
-    }
-  );
-
-  const { runAsync: onAccept, loading: isLoadingAccept } = useRequest2(updateInviteResult, {
-    onSuccess() {
-      toast({
-        status: 'success',
-        title: t('common:user.team.invite.Accepted')
-      });
-      fetchInviteList();
-      initUserInfo();
-    }
-  });
-  const { runAsync: onReject, loading: isLoadingReject } = useRequest2(updateInviteResult, {
-    onSuccess() {
-      toast({
-        status: 'success',
-        title: t('common:user.team.invite.Reject')
-      });
-      fetchInviteList();
-      initUserInfo();
-    }
-  });
-
-  return (
-    <MyModal
-      isOpen={inviteList && inviteList.length > 0}
-      iconSrc="/imgs/modal/team.svg"
-      title={
-        <Box>
-          <Box>{t('common:user.team.Processing invitations')}</Box>
-          <Box fontWeight={'normal'} fontSize={'sm'} color={'myGray.500'}>
-            {t('common:user.team.Processing invitations Tips', { amount: inviteList?.length })}
-          </Box>
-        </Box>
-      }
-      maxW={['90vw', '500px']}
-    >
-      <ModalBody>
-        {inviteList?.map((item) => (
-          <Flex
-            key={item.teamId}
-            alignItems={'center'}
-            border={theme.borders.base}
-            borderRadius={'md'}
-            px={3}
-            py={2}
-            _notFirst={{
-              mt: 3
-            }}
-          >
-            <Avatar src={item.avatar} w={['16px', '23px']} />
-            <Box mx={2}>{item.teamName}</Box>
-            <Box flex={1} />
-            <Button
-              size="sm"
-              variant={'solid'}
-              colorScheme="green"
-              isLoading={isLoadingAccept}
-              onClick={() => {
-                openConfirm(
-                  () =>
-                    onAccept({
-                      tmbId: item.tmbId,
-                      status: TeamMemberStatusEnum.active
-                    }),
-                  undefined,
-                  t('common:user.team.invite.Accept Confirm')
-                )();
-              }}
-            >
-              {t('common:user.team.invite.accept')}
-            </Button>
-            <Button
-              size="sm"
-              ml={2}
-              variant={'solid'}
-              colorScheme="red"
-              isLoading={isLoadingReject}
-              onClick={() => {
-                openConfirm(
-                  () =>
-                    onReject({
-                      tmbId: item.tmbId,
-                      status: TeamMemberStatusEnum.reject
-                    }),
-                  undefined,
-                  t('common:user.team.invite.Reject Confirm')
-                )();
-              }}
-            >
-              {t('common:user.team.invite.reject')}
-            </Button>
-          </Flex>
-        ))}
-      </ModalBody>
-      <ModalFooter justifyContent={'center'}>
-        <Box>{t('common:user.team.invite.Deal Width Footer Tip')}</Box>
-      </ModalFooter>
-
-      <ConfirmModal />
-    </MyModal>
-  );
-};
-
-export default React.memo(UpdateInviteModal);
diff --git a/projects/app/src/pageComponents/account/team/CreateInvitationModal.tsx b/projects/app/src/pageComponents/account/team/CreateInvitationModal.tsx
new file mode 100644
index 000000000000..7d824f467d50
--- /dev/null
+++ b/projects/app/src/pageComponents/account/team/CreateInvitationModal.tsx
@@ -0,0 +1,99 @@
+import { postCreateInvitationLink } from '@/web/support/user/team/api';
+import {
+  Box,
+  Button,
+  Grid,
+  HStack,
+  Input,
+  ModalBody,
+  ModalCloseButton,
+  ModalFooter
+} from '@chakra-ui/react';
+import {
+  InvitationLinkCreateType,
+  InvitationLinkExpiresType
+} from '@fastgpt/service/support/user/team/invitationLink/type';
+import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel';
+import MyModal from '@fastgpt/web/components/common/MyModal';
+import MySelect from '@fastgpt/web/components/common/MySelect';
+import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
+import { useTranslation } from 'next-i18next';
+import { useForm } from 'react-hook-form';
+
+function CreateInvitationModal({ onClose }: { onClose: () => void }) {
+  const { t } = useTranslation();
+  const expiresOptions: Array<{ label: string; value: InvitationLinkExpiresType }> = [
+    { label: t('account_team:30mins'), value: '30m' }, // 30 mins
+    { label: t('account_team:7days'), value: '7d' }, // 7 days
+    { label: t('account_team:1year'), value: '1y' } // 1 year
+  ];
+
+  const usedTimesLimitOptions = [
+    { label: t('account_team:unlimited'), value: -1 },
+    { label: t('account_team:1person'), value: 1 }
+  ];
+  const { register, handleSubmit, watch, setValue } = useForm<InvitationLinkCreateType>({
+    defaultValues: {
+      description: '',
+      expires: expiresOptions[1].value,
+      usedTimesLimit: usedTimesLimitOptions[1].value
+    }
+  });
+
+  const expires = watch('expires');
+  const usedTimesLimit = watch('usedTimesLimit');
+
+  const { runAsync: createInvitationLink } = useRequest2(postCreateInvitationLink, {
+    manual: true,
+    successToast: t('common:common.Create Success'),
+    errorToast: t('common:common.Create Failed'),
+    onFinally: () => onClose()
+  });
+
+  return (
+    <MyModal
+      isOpen
+      iconSrc="common/addLight"
+      iconColor="primary.500"
+      title={<Box>{t('account_team:create_invitation_link')}</Box>}
+      minW={'500px'}
+    >
+      <ModalCloseButton onClick={onClose} />
+      <ModalBody>
+        <Grid gap={4} w="full" templateColumns="max-content 1fr" alignItems="center">
+          <FormLabel required={true}>{t('account_team:invitation_link_description')}</FormLabel>
+          <Input
+            placeholder={t('account_team:invitation_link_description')}
+            {...register('description', { required: true })}
+          />
+
+          <FormLabel required={true}>{t('account_team:expires')}</FormLabel>
+          <MySelect
+            list={expiresOptions}
+            value={expires}
+            onchange={(val) => setValue('expires', val)}
+            minW="120px"
+          />
+
+          <FormLabel required={true}>{t('account_team:used_times_limit')}</FormLabel>
+          <MySelect
+            list={usedTimesLimitOptions}
+            value={usedTimesLimit}
+            onchange={(val) => setValue('usedTimesLimit', val)}
+            minW="120px"
+          />
+        </Grid>
+      </ModalBody>
+      <ModalFooter>
+        <Button isLoading={false} onClick={onClose} variant="outline">
+          {t('common:common.Cancel')}
+        </Button>
+        <Button isLoading={false} onClick={handleSubmit(createInvitationLink)} ml="4">
+          {t('common:common.Confirm')}
+        </Button>
+      </ModalFooter>
+    </MyModal>
+  );
+}
+
+export default CreateInvitationModal;
diff --git a/projects/app/src/pageComponents/account/team/GroupManage/index.tsx b/projects/app/src/pageComponents/account/team/GroupManage/index.tsx
index bd568d815431..dc9ae458c90b 100644
--- a/projects/app/src/pageComponents/account/team/GroupManage/index.tsx
+++ b/projects/app/src/pageComponents/account/team/GroupManage/index.tsx
@@ -172,7 +172,7 @@ function MemberTable({ Tabs }: { Tabs: React.ReactNode }) {
                   </Td>
                   <Td>
                     {group.name === DefaultGroupName ? (
-                      <AvatarGroup avatars={members.map((v) => v.avatar)} groupId={group._id} />
+                      <AvatarGroup avatars={members.map((v) => v.avatar)} />
                     ) : hasGroupManagePer(group) ? (
                       <MyTooltip label={t('account_team:manage_member')}>
                         <Box cursor="pointer" onClick={() => onManageMember(group)}>
@@ -180,7 +180,6 @@ function MemberTable({ Tabs }: { Tabs: React.ReactNode }) {
                             avatars={group.members.map(
                               (v) => members.find((m) => m.tmbId === v.tmbId)?.avatar ?? ''
                             )}
-                            groupId={group._id}
                           />
                         </Box>
                       </MyTooltip>
@@ -189,7 +188,6 @@ function MemberTable({ Tabs }: { Tabs: React.ReactNode }) {
                         avatars={group.members.map(
                           (v) => members.find((m) => m.tmbId === v.tmbId)?.avatar ?? ''
                         )}
-                        groupId={group._id}
                       />
                     )}
                   </Td>
diff --git a/projects/app/src/pageComponents/account/team/HandleInviteModal.tsx b/projects/app/src/pageComponents/account/team/HandleInviteModal.tsx
new file mode 100644
index 000000000000..40fc5d35c10f
--- /dev/null
+++ b/projects/app/src/pageComponents/account/team/HandleInviteModal.tsx
@@ -0,0 +1,106 @@
+import { getInvitationInfo, postAcceptInvitationLink } from '@/web/support/user/team/api';
+import {
+  Box,
+  Button,
+  CloseButton,
+  Flex,
+  ModalBody,
+  ModalCloseButton,
+  ModalHeader
+} from '@chakra-ui/react';
+import Avatar from '@fastgpt/web/components/common/Avatar';
+import MyModal from '@fastgpt/web/components/common/MyModal';
+import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
+import { useTranslation } from 'next-i18next';
+import { useRouter } from 'next/router';
+import { useCallback } from 'react';
+import { useContextSelector } from 'use-context-selector';
+import { TeamContext } from './context';
+import { isForbidden } from '@fastgpt/service/support/user/team/invitationLink/controllers';
+import { useToast } from '@fastgpt/web/hooks/useToast';
+
+function Invite({ invitelinkid }: { invitelinkid: string }) {
+  const router = useRouter();
+  const { t } = useTranslation();
+
+  const { onSwitchTeam, refetchMembers } = useContextSelector(TeamContext, (v) => v);
+
+  const onClose = () => {
+    router.push('/account/team');
+  };
+
+  const { toast } = useToast();
+
+  const { data: invitationInfo } = useRequest2(() => getInvitationInfo(invitelinkid), {
+    manual: false,
+    onSuccess: (data) => {
+      if (isForbidden(data)) {
+        toast({
+          description: t('account_team:invitation_link_has_been_invalid'),
+          status: 'warning'
+        });
+        onClose();
+      }
+    },
+    onError: onClose
+  });
+
+  const { runAsync: acceptInvitation } = useRequest2(() => postAcceptInvitationLink(invitelinkid), {
+    manual: true,
+    successToast: t('common:common.Success'),
+    onSuccess: () => {
+      toast({
+        description: t('common:common.Success'),
+        status: 'success'
+      });
+      onSwitchTeam(invitationInfo!.teamId);
+      refetchMembers();
+      onClose();
+    },
+    onError: (e) => {
+      toast({
+        description: t('common:common.Error'),
+        status: 'error'
+      });
+      onClose();
+    }
+  });
+
+  return (
+    <>
+      {invitationInfo && (
+        <MyModal
+          isOpen={true}
+          iconSrc="support/user/usersLight"
+          title={t('account_team:handle_invitation')}
+          iconColor={'primary.600'}
+        >
+          <ModalCloseButton onClick={onClose} />
+          <ModalBody>
+            <Flex
+              key={invitationInfo._id}
+              alignItems={'center'}
+              border={'1px solid'}
+              borderColor={'myGray.200'}
+              borderRadius={'md'}
+              px={3}
+              py={2}
+            >
+              <Avatar src={invitationInfo.teamAvatar} w={['16px', '23px']} />
+              <Box mx={2}>{invitationInfo.teamName}</Box>
+              <Box flex={1} />
+              <Button size="sm" variant={'solid'} colorScheme="green" onClick={acceptInvitation}>
+                {t('common:user.team.invite.accept')}
+              </Button>
+              <Button size="sm" ml={2} variant="outline" onClick={onClose}>
+                {t('account_team:ignore')}
+              </Button>
+            </Flex>
+          </ModalBody>
+        </MyModal>
+      )}
+    </>
+  );
+}
+
+export default Invite;
diff --git a/projects/app/src/pageComponents/account/team/InviteModal.tsx b/projects/app/src/pageComponents/account/team/InviteModal.tsx
index 916ebae21f36..66ed547ef814 100644
--- a/projects/app/src/pageComponents/account/team/InviteModal.tsx
+++ b/projects/app/src/pageComponents/account/team/InviteModal.tsx
@@ -1,12 +1,41 @@
-import React, { useState } from 'react';
+import MemberTag from '@/components/support/user/team/Info/MemberTag';
+import Empty from '@/pageComponents/chat/Empty';
+import { getInvitationLinkList, putUpdateInvitationInfo } from '@/web/support/user/team/api';
+import {
+  Box,
+  Button,
+  Divider,
+  Flex,
+  Grid,
+  HStack,
+  ModalBody,
+  ModalCloseButton,
+  ModalFooter,
+  ModalHeader,
+  Table,
+  TableContainer,
+  Tbody,
+  Td,
+  Th,
+  Thead,
+  Tr,
+  useDisclosure
+} from '@chakra-ui/react';
+import AvatarGroup from '@fastgpt/web/components/common/Avatar/AvatarGroup';
+import EmptyTip from '@fastgpt/web/components/common/EmptyTip';
+import Icon from '@fastgpt/web/components/common/Icon';
+import MyIconButton from '@fastgpt/web/components/common/Icon/button';
 import MyModal from '@fastgpt/web/components/common/MyModal';
-import { useTranslation } from 'next-i18next';
-import { ModalCloseButton, ModalBody, Box, ModalFooter, Button } from '@chakra-ui/react';
-import TagTextarea from '@/components/common/Textarea/TagTextarea';
+import MyPopover from '@fastgpt/web/components/common/MyPopover';
+import Tag from '@fastgpt/web/components/common/Tag';
+import { useCopyData } from '@fastgpt/web/hooks/useCopyData';
 import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
-import { postInviteTeamMember } from '@/web/support/user/team/api';
-import { useConfirm } from '@fastgpt/web/hooks/useConfirm';
-import type { InviteMemberResponse } from '@fastgpt/global/support/user/team/controller.d';
+import format from 'date-fns/format';
+import { useTranslation } from 'next-i18next';
+import dynamic from 'next/dynamic';
+import { useCallback } from 'react';
+
+const CreateInvitationModal = dynamic(() => import('./CreateInvitationModal'));
 
 const InviteModal = ({
   teamId,
@@ -18,43 +47,47 @@ const InviteModal = ({
   onSuccess: () => void;
 }) => {
   const { t } = useTranslation();
-  const { ConfirmModal, openConfirm } = useConfirm({
-    title: t('user:team.Invite Member Result Tip'),
-    showCancel: false
+
+  const {
+    data: invitationLinkList,
+    loading: isLoadingLink,
+    runAsync: refetchInvitationLinkList
+  } = useRequest2(() => getInvitationLinkList(), {
+    manual: false
   });
 
-  const [inviteUsernames, setInviteUsernames] = useState<string[]>([]);
+  const { isOpen: isOpenCreate, onOpen: onOpenCreate, onClose: onCloseCreate } = useDisclosure();
 
-  const { runAsync: onInvite, loading: isLoading } = useRequest2(
-    () =>
-      postInviteTeamMember({
-        teamId,
-        usernames: inviteUsernames
+  const isLoading = isLoadingLink;
+  const { copyData } = useCopyData();
+
+  const onCopy = useCallback(
+    (linkId: string) => {
+      copyData(location.origin + `/account/team?invitelinkid=${linkId}`);
+    },
+    [copyData]
+  );
+
+  const { runAsync: onForbid } = useRequest2(
+    (linkId: string) =>
+      putUpdateInvitationInfo({
+        linkId,
+        forbidden: true
       }),
     {
-      onSuccess(res: InviteMemberResponse) {
-        onSuccess();
-        openConfirm(
-          () => onClose(),
-          undefined,
-          <Box whiteSpace={'pre-wrap'}>
-            {t('user:team.Invite Member Success Tip', {
-              success: res.invite.length,
-              inValid: res.inValid.map((item) => item.username).join(', '),
-              inTeam: res.inTeam.map((item) => item.username).join(', ')
-            })}
-          </Box>
-        )();
-      },
-      errorToast: t('user:team.Invite Member Failed Tip')
+      manual: true,
+      onSuccess: refetchInvitationLinkList,
+      successToast: t('account_team:forbid_success')
     }
   );
 
   return (
     <MyModal
+      isLoading={isLoading}
       isOpen
       iconSrc="common/inviteLight"
       iconColor="primary.600"
+      minW={'600px'}
       title={
         <Box>
           <Box>{t('common:user.team.Invite Member')}</Box>
@@ -63,26 +96,177 @@ const InviteModal = ({
           </Box>
         </Box>
       }
-      maxW={['90vw', '400px']}
+      maxW={['90vw']}
       overflow={'unset'}
     >
       <ModalCloseButton onClick={onClose} />
-      <ModalBody>
-        <Box mb={2}>{t('common:user.Account')}</Box>
-        <TagTextarea defaultValues={inviteUsernames} onUpdate={setInviteUsernames} />
+      <ModalHeader pb="0">
+        <Flex alignItems={'center'} justifyContent={'space-between'} mx="2">
+          <HStack>
+            <Icon name="common/list" w="16px" />
+            <Box ml="6px" fontSize="md">
+              {t('account_team:invitation_link_list')}
+            </Box>
+          </HStack>
+          <Button onClick={onOpenCreate}>{t('account_team:create_invitation_link')}</Button>
+        </Flex>
+      </ModalHeader>
+      <ModalBody maxH="500px">
+        <TableContainer overflowY={'auto'}>
+          <Table fontSize={'sm'} overflow={'unset'}>
+            <Thead>
+              <Tr bgColor={'white !important'}>
+                <Th borderLeftRadius="6px" bgColor="myGray.100">
+                  {t('account_team:invitation_link_description')}
+                </Th>
+                <Th bgColor="myGray.100">{t('account_team:expires')}</Th>
+                <Th bgColor="myGray.100">{t('account_team:used_times_limit')}</Th>
+                <Th bgColor="myGray.100">{t('account_team:invited')}</Th>
+                <Th bgColor="myGray.100" borderRightRadius="6px">
+                  {t('common:common.Action')}
+                </Th>
+              </Tr>
+            </Thead>
+            {!!invitationLinkList?.length && (
+              <Tbody overflow={'unset'}>
+                {invitationLinkList?.map((item) => {
+                  const isForbidden = item.forbidden || new Date(item.expires) < new Date();
+                  return (
+                    <Tr key={item._id} overflow={'unset'}>
+                      <Td maxW="200px" minW="100px">
+                        {item.description}
+                      </Td>
+                      <Td>
+                        {isForbidden ? (
+                          <Tag colorSchema="gray">{t('account_team:has_forbidden')}</Tag>
+                        ) : (
+                          format(new Date(item.expires), 'yyyy-MM-dd HH:mm')
+                        )}
+                      </Td>
+                      <Td>
+                        {item.usedTimesLimit === -1
+                          ? t('account_team:unlimited')
+                          : item.usedTimesLimit}
+                      </Td>
+                      <Td>
+                        <MyPopover
+                          w="fit-content"
+                          Trigger={
+                            <Box
+                              minW="100px"
+                              borderRadius="md"
+                              cursor="pointer"
+                              _hover={{ bg: 'myGray.100' }}
+                              p="1.5"
+                              w="fit-content"
+                            >
+                              <AvatarGroup max={3} avatars={item.members.map((i) => i.avatar)} />
+                            </Box>
+                          }
+                          trigger="click"
+                          closeOnBlur={true}
+                        >
+                          {() => (
+                            <Box py="4" maxH="200px" w="fit-content">
+                              <Flex mx="4" justifyContent="center" alignItems={'center'}>
+                                <Box>{t('account_team:has_invited')}</Box>
+                                <Box
+                                  ml="auto"
+                                  bg="myGray.200"
+                                  px="2"
+                                  borderRadius="md"
+                                  fontSize="sm"
+                                >
+                                  {item.members.length}
+                                </Box>
+                              </Flex>
+                              <Divider my="2" mx="4" />
+                              <Grid
+                                w="fit-content"
+                                mt="2"
+                                gridRowGap="4"
+                                gridTemplateColumns="1fr 1fr"
+                                overflow="auto"
+                                alignItems="center"
+                                mx="4"
+                              >
+                                {item.members.map((member) => (
+                                  <Box key={member.tmbId} justifySelf="start">
+                                    <MemberTag name={member.name} avatar={member.avatar} />
+                                  </Box>
+                                ))}
+                              </Grid>
+                            </Box>
+                          )}
+                        </MyPopover>
+                      </Td>
+                      <Td>
+                        {!isForbidden && (
+                          <>
+                            <Button
+                              size="sm"
+                              variant="outline"
+                              onClick={() => onCopy(item._id)}
+                              color="myGray.900"
+                            >
+                              <Icon name="common/link" w="16px" mr="1" />
+                              {t('account_team:copy_link')}
+                            </Button>
+                            <MyPopover
+                              placement="bottom-end"
+                              Trigger={
+                                <Button variant="outline" ml="10px" size="sm" color="myGray.900">
+                                  <Icon name="common/lineStop" w="16px" mr="1" />
+                                  {t('account_team:forbidden')}
+                                </Button>
+                              }
+                              closeOnBlur={true}
+                            >
+                              {({ onClose: onClosePopover }) => (
+                                <Box p={4}>
+                                  <Box fontWeight={400} whiteSpace="pre-wrap">
+                                    {t('account_team:forbid_hint')}
+                                  </Box>
+                                  <Flex gap={2} mt={2} justifyContent={'flex-end'}>
+                                    <Button variant="outline" onClick={onClosePopover}>
+                                      {t('common:common.Cancel')}
+                                    </Button>
+                                    <Button
+                                      variant="outline"
+                                      colorScheme="red"
+                                      onClick={() => {
+                                        onForbid(item._id);
+                                        onClosePopover();
+                                      }}
+                                    >
+                                      {t('account_team:confirm_forbidden')}
+                                    </Button>
+                                  </Flex>
+                                </Box>
+                              )}
+                            </MyPopover>
+                          </>
+                        )}
+                      </Td>
+                    </Tr>
+                  );
+                })}
+              </Tbody>
+            )}
+          </Table>
+          {!invitationLinkList?.length && <EmptyTip />}
+        </TableContainer>
       </ModalBody>
-      <ModalFooter>
-        <Button
-          w={'100%'}
-          h={'34px'}
-          isDisabled={inviteUsernames.length === 0}
-          isLoading={isLoading}
-          onClick={onInvite}
-        >
-          {t('user:team.Confirm Invite')}
-        </Button>
+      <ModalFooter justifyContent={'flex-start'}>
+        <Tag colorSchema="blue" marginBlock="2">
+          <Box>{t('account_team:invitation_link_auto_clean_hint')}</Box>
+        </Tag>
       </ModalFooter>
-      <ConfirmModal />
+      {isOpenCreate && (
+        <CreateInvitationModal
+          onClose={() => Promise.all([onCloseCreate(), refetchInvitationLinkList()])}
+        />
+      )}
     </MyModal>
   );
 };
diff --git a/projects/app/src/pageComponents/account/team/MemberTable.tsx b/projects/app/src/pageComponents/account/team/MemberTable.tsx
index fe9968da87a4..d3f9ee12cd5f 100644
--- a/projects/app/src/pageComponents/account/team/MemberTable.tsx
+++ b/projects/app/src/pageComponents/account/team/MemberTable.tsx
@@ -17,7 +17,7 @@ import {
 import { useTranslation } from 'next-i18next';
 import { useUserStore } from '@/web/support/user/useUserStore';
 import { useConfirm } from '@fastgpt/web/hooks/useConfirm';
-import { delRemoveMember, updateStatus } from '@/web/support/user/team/api';
+import { delRemoveMember, postRestoreMember } from '@/web/support/user/team/api';
 import Tag from '@fastgpt/web/components/common/Tag';
 import Icon from '@fastgpt/web/components/common/Icon';
 import { useContextSelector } from 'use-context-selector';
@@ -118,7 +118,7 @@ function MemberTable({ Tabs }: { Tabs: React.ReactNode }) {
     errorToast: t('account_team:sync_member_failed')
   });
 
-  const { runAsync: onRestore, loading: isUpdateInvite } = useRequest2(updateStatus, {
+  const { runAsync: onRestore, loading: isUpdateInvite } = useRequest2(postRestoreMember, {
     onSuccess() {
       refetchMembers();
     },
@@ -253,12 +253,7 @@ function MemberTable({ Tabs }: { Tabs: React.ReactNode }) {
                           <Avatar src={member.avatar} w={['18px', '22px']} borderRadius={'50%'} />
                           <Box className={'textEllipsis'}>
                             {member.memberName}
-                            {member.status === 'waiting' && (
-                              <Tag ml="2" colorSchema="yellow">
-                                {t('account_team:waiting')}
-                              </Tag>
-                            )}
-                            {member.status === 'leave' && (
+                            {member.status !== 'active' && (
                               <Tag ml="2" colorSchema="gray">
                                 {t('account_team:leave')}
                               </Tag>
@@ -295,7 +290,7 @@ function MemberTable({ Tabs }: { Tabs: React.ReactNode }) {
                         {userInfo?.team.permission.hasManagePer &&
                           member.role !== TeamMemberRoleEnum.owner &&
                           member.tmbId !== userInfo?.team.tmbId &&
-                          (member.status !== TeamMemberStatusEnum.leave ? (
+                          (member.status === TeamMemberStatusEnum.active ? (
                             <Icon
                               name={'common/trash'}
                               cursor={'pointer'}
@@ -320,30 +315,28 @@ function MemberTable({ Tabs }: { Tabs: React.ReactNode }) {
                               }}
                             />
                           ) : (
-                            <Icon
-                              name={'common/confirm/restoreTip'}
-                              cursor={'pointer'}
-                              w="1rem"
-                              p="1"
-                              borderRadius="sm"
-                              _hover={{
-                                color: 'primary.500',
-                                bgColor: 'myGray.100'
-                              }}
-                              onClick={() => {
-                                openRestoreMember(
-                                  () =>
-                                    onRestore({
-                                      tmbId: member.tmbId,
-                                      status: TeamMemberStatusEnum.active
-                                    }),
-                                  undefined,
-                                  t('account_team:restore_tip', {
-                                    username: member.memberName
-                                  })
-                                )();
-                              }}
-                            />
+                            member.status === TeamMemberStatusEnum.forbidden && (
+                              <Icon
+                                name={'common/confirm/restoreTip'}
+                                cursor={'pointer'}
+                                w="1rem"
+                                p="1"
+                                borderRadius="sm"
+                                _hover={{
+                                  color: 'primary.500',
+                                  bgColor: 'myGray.100'
+                                }}
+                                onClick={() => {
+                                  openRestoreMember(
+                                    () => onRestore(member.tmbId),
+                                    undefined,
+                                    t('account_team:restore_tip', {
+                                      username: member.memberName
+                                    })
+                                  )();
+                                }}
+                              />
+                            )
                           ))}
                       </Td>
                     </Tr>
diff --git a/projects/app/src/pages/account/team/index.tsx b/projects/app/src/pages/account/team/index.tsx
index 37b3b4ce697b..257fc2a7d782 100644
--- a/projects/app/src/pages/account/team/index.tsx
+++ b/projects/app/src/pages/account/team/index.tsx
@@ -20,6 +20,7 @@ const PermissionManage = dynamic(
 );
 const GroupManage = dynamic(() => import('@/pageComponents/account/team/GroupManage/index'));
 const OrgManage = dynamic(() => import('@/pageComponents/account/team/OrgManage/index'));
+const HandleInviteModal = dynamic(() => import('@/pageComponents/account/team/HandleInviteModal'));
 
 export enum TeamTabEnum {
   member = 'member',
@@ -30,6 +31,16 @@ export enum TeamTabEnum {
 
 const Team = () => {
   const router = useRouter();
+
+  const invitelinkid = useMemo(() => {
+    const _id = router.query.invitelinkid;
+    if (!_id && typeof _id !== 'string') {
+      return '';
+    } else {
+      return _id as string;
+    }
+  }, [router.query.invitelinkid]);
+
   const { teamTab = TeamTabEnum.member } = router.query as { teamTab: `${TeamTabEnum}` };
 
   const { t } = useTranslation();
@@ -142,6 +153,7 @@ const Team = () => {
           {teamTab === TeamTabEnum.permission && <PermissionManage Tabs={Tabs} />}
         </Box>
       </Flex>
+      {invitelinkid && <HandleInviteModal invitelinkid={invitelinkid} />}
     </AccountContainer>
   );
 };
diff --git a/projects/app/src/web/support/user/team/api.ts b/projects/app/src/web/support/user/team/api.ts
index aa857a356b1c..058e1fe3ce2d 100644
--- a/projects/app/src/web/support/user/team/api.ts
+++ b/projects/app/src/web/support/user/team/api.ts
@@ -9,7 +9,6 @@ import {
   InviteMemberProps,
   InviteMemberResponse,
   UpdateInviteProps,
-  UpdateStatusProps,
   UpdateTeamProps
 } from '@fastgpt/global/support/user/team/controller.d';
 import type { TeamTagItemType, TeamTagSchema } from '@fastgpt/global/support/user/team/type';
@@ -21,6 +20,12 @@ import {
 import { FeTeamPlanStatusType, TeamSubSchema } from '@fastgpt/global/support/wallet/sub/type';
 import { TeamInvoiceHeaderType } from '@fastgpt/global/support/user/team/type';
 import { PaginationProps, PaginationResponse } from '@fastgpt/web/common/fetch/type';
+import {
+  InvitationInfoType,
+  InvitationLinkCreateType,
+  InvitationLinkUpdateType,
+  InvitationType
+} from '@fastgpt/service/support/user/team/invitationLink/type';
 
 /* --------------- team  ---------------- */
 export const getTeamList = (status: `${TeamMemberSchema['status']}`) =>
@@ -34,18 +39,37 @@ export const putSwitchTeam = (teamId: string) =>
 /* --------------- team member ---------------- */
 export const getTeamMembers = (props: PaginationProps<{ withLeaved?: boolean }>) =>
   GET<PaginationResponse<TeamMemberItemType>>(`/proApi/support/user/team/member/list`, props);
-export const postInviteTeamMember = (data: InviteMemberProps) =>
-  POST<InviteMemberResponse>(`/proApi/support/user/team/member/invite`, data);
+
+// export const postInviteTeamMember = (data: InviteMemberProps) =>
+//   POST<InviteMemberResponse>(`/proApi/support/user/team/member/invite`, data);
+
 export const putUpdateMemberName = (name: string) =>
   PUT(`/proApi/support/user/team/member/updateName`, { name });
 export const delRemoveMember = (tmbId: string) =>
   DELETE(`/proApi/support/user/team/member/delete`, { tmbId });
 export const updateInviteResult = (data: UpdateInviteProps) =>
   PUT('/proApi/support/user/team/member/updateInvite', data);
-export const updateStatus = (data: UpdateStatusProps) =>
-  PUT('/proApi/support/user/team/member/updateStatus', data);
+export const postRestoreMember = (tmbId: string) =>
+  POST('/proApi/support/user/team/member/restore', { tmbId });
 export const delLeaveTeam = () => DELETE('/proApi/support/user/team/member/leave');
 
+/* -------------- team invitaionlink -------------------- */
+
+export const postCreateInvitationLink = (data: InvitationLinkCreateType) =>
+  POST<string>(`/proApi/support/user/team/invitationLink/create`, data);
+
+export const getInvitationLinkList = () =>
+  GET<InvitationType[]>(`/proApi/support/user/team/invitationLink/list`);
+
+export const postAcceptInvitationLink = (linkId: string) =>
+  POST<string>(`/proApi/support/user/team/invitationLink/accept`, { linkId });
+
+export const getInvitationInfo = (linkId: string) =>
+  GET<InvitationInfoType>(`/proApi/support/user/team/invitationLink/info`, { linkId });
+
+export const putUpdateInvitationInfo = (data: InvitationLinkUpdateType) =>
+  PUT('/proApi/support/user/team/invitationLink/update', data);
+
 /* -------------- team collaborator -------------------- */
 export const getTeamClbs = () =>
   GET<CollaboratorItemType[]>(`/proApi/support/user/team/collaborator/list`);


Reviews:
user: login: c121914yu
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-11T08:59:11Z
user: login: c121914yu
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-11T09:01:01Z
user: login: FinleyGe
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-12T02:05:32Z
user: login: c121914yu
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-03-12T02:14:10Z

Review comments:
diff_hunk: @@ -320,30 +315,28 @@ function MemberTable({ Tabs }: { Tabs: React.ReactNode }) {
                               }}
                             />
                           ) : (
-                            <Icon
-                              name={'common/confirm/restoreTip'}
-                              cursor={'pointer'}
-                              w="1rem"
-                              p="1"
-                              borderRadius="sm"
-                              _hover={{
-                                color: 'primary.500',
-                                bgColor: 'myGray.100'
-                              }}
-                              onClick={() => {
-                                openRestoreMember(
-                                  () =>
-                                    onRestore({
-                                      tmbId: member.tmbId,
-                                      status: TeamMemberStatusEnum.active
-                                    }),
-                                  undefined,
-                                  t('account_team:restore_tip', {
-                                    username: member.memberName
-                                  })
-                                )();
-                              }}
-                            />
+                            member.status === TeamMemberStatusEnum.forbidden && (
path: projects/app/src/pageComponents/account/team/MemberTable.tsx
user: login: c121914yu
body: 好像还得判断下是不是同步模式
created_at: 2025-03-11T08:59:10Z
author_association: COLLABORATOR
path: projects/app/src/pages/account/team/index.tsx
user: login: c121914yu
body: 这个页面为啥需要invitelinkid
created_at: 2025-03-11T09:00:54Z
author_association: COLLABORATOR
diff_hunk: @@ -320,30 +315,28 @@ function MemberTable({ Tabs }: { Tabs: React.ReactNode }) {
                               }}
                             />
                           ) : (
-                            <Icon
-                              name={'common/confirm/restoreTip'}
-                              cursor={'pointer'}
-                              w="1rem"
-                              p="1"
-                              borderRadius="sm"
-                              _hover={{
-                                color: 'primary.500',
-                                bgColor: 'myGray.100'
-                              }}
-                              onClick={() => {
-                                openRestoreMember(
-                                  () =>
-                                    onRestore({
-                                      tmbId: member.tmbId,
-                                      status: TeamMemberStatusEnum.active
-                                    }),
-                                  undefined,
-                                  t('account_team:restore_tip', {
-                                    username: member.memberName
-                                  })
-                                )();
-                              }}
-                            />
+                            member.status === TeamMemberStatusEnum.forbidden && (
path: projects/app/src/pageComponents/account/team/MemberTable.tsx
user: login: FinleyGe
body: 不用应该。。。 forbidden 就是可以恢复的。。不是同步模式也不会出现 forbidden 的情况了
如果是同步模式下 forbidden 了一个人，然后改到不是同步模式了，感觉恢复这个人也是合理的吧
created_at: 2025-03-12T02:05:32Z
author_association: COLLABORATOR

PR comments:
user: login: c121914yu
created_at: 2025-03-05T04:16:30Z
body: # 🤖 Generated by deploy action
📦 Preview Image: `ghcr.io/labring/fastgpt-pr:37b7b6021c168df669304c15f27ce528e76b5e59`

<!-- DEFAULT_REPLACE_DEPLOY -->
author_association: COLLABORATOR
user: login: gru-agent[bot]
created_at: 2025-03-11T09:01:26Z
body: ❌ Rebase test pr failed: Error: Pull request labring/FastGPT#3979 is not created by gru-agent[bot]
author_association: CONTRIBUTOR