feat: add app log permission (#4932)

* feat: add app log permission

* fix: org search bug
This commit is contained in:
Finley Ge 2025-05-30 17:09:29 +08:00 committed by archer
parent 5a5367d30b
commit d7b9f94270
No known key found for this signature in database
GPG Key ID: 4446499B846D4A9E
10 changed files with 119 additions and 73 deletions

View File

@ -1,8 +1,10 @@
import { NullPermission, PermissionKeyEnum, PermissionList } from '../constant'; import { NullPermission, PermissionKeyEnum, PermissionList } from '../constant';
import { type PermissionListType } from '../type'; import { type PermissionListType } from '../type';
import { i18nT } from '../../../../web/i18n/utils'; import { i18nT } from '../../../../web/i18n/utils';
export enum AppPermissionKeyEnum {} export enum AppPermissionKeyEnum {
export const AppPermissionList: PermissionListType = { log = 'log'
}
export const AppPermissionList: PermissionListType<AppPermissionKeyEnum> = {
[PermissionKeyEnum.read]: { [PermissionKeyEnum.read]: {
...PermissionList[PermissionKeyEnum.read], ...PermissionList[PermissionKeyEnum.read],
description: i18nT('app:permission.des.read') description: i18nT('app:permission.des.read')
@ -13,8 +15,16 @@ export const AppPermissionList: PermissionListType = {
}, },
[PermissionKeyEnum.manage]: { [PermissionKeyEnum.manage]: {
...PermissionList[PermissionKeyEnum.manage], ...PermissionList[PermissionKeyEnum.manage],
value: 0b1111,
description: i18nT('app:permission.des.manage') description: i18nT('app:permission.des.manage')
},
[AppPermissionKeyEnum.log]: {
name: i18nT('app:permission.name.log'),
value: 0b1000,
checkBoxType: 'multiple',
description: i18nT('app:permission.des.log')
} }
}; };
export const AppDefaultPermissionVal = NullPermission; export const AppDefaultPermissionVal = NullPermission;
export const AppLogPermissionVal = AppPermissionList[AppPermissionKeyEnum.log].value;

View File

@ -1,7 +1,8 @@
import { type PerConstructPros, Permission } from '../controller'; import { type PerConstructPros, Permission } from '../controller';
import { AppDefaultPermissionVal } from './constant'; import { AppDefaultPermissionVal, AppPermissionList } from './constant';
export class AppPermission extends Permission { export class AppPermission extends Permission {
hasLogPer: boolean = false;
constructor(props?: PerConstructPros) { constructor(props?: PerConstructPros) {
if (!props) { if (!props) {
props = { props = {
@ -11,5 +12,11 @@ export class AppPermission extends Permission {
props.per = AppDefaultPermissionVal; props.per = AppDefaultPermissionVal;
} }
super(props); super(props);
this.setUpdatePermissionCallback(() => {
this.hasReadPer = this.checkPer(AppPermissionList.read.value);
this.hasWritePer = this.checkPer(AppPermissionList.write.value);
this.hasManagePer = this.checkPer(AppPermissionList.manage.value);
this.hasLogPer = this.checkPer(AppPermissionList.log.value);
});
} }
} }

View File

@ -123,6 +123,8 @@
"permission.des.manage": "Based on write permissions, you can configure publishing channels, view conversation logs, and assign permissions to the application.", "permission.des.manage": "Based on write permissions, you can configure publishing channels, view conversation logs, and assign permissions to the application.",
"permission.des.read": "Use the app to have conversations", "permission.des.read": "Use the app to have conversations",
"permission.des.write": "Can view and edit apps", "permission.des.write": "Can view and edit apps",
"permission.des.log": "Can view conversation logs",
"permission.name.log": "View logs",
"plugin.Instructions": "Instructions", "plugin.Instructions": "Instructions",
"plugin_cost_by_token": "Charged based on token usage", "plugin_cost_by_token": "Charged based on token usage",
"plugin_cost_per_times": "{{cost}} points/time", "plugin_cost_per_times": "{{cost}} points/time",

View File

@ -123,6 +123,8 @@
"permission.des.manage": "写权限基础上,可配置发布渠道、查看对话日志、分配该应用权限", "permission.des.manage": "写权限基础上,可配置发布渠道、查看对话日志、分配该应用权限",
"permission.des.read": "可使用该应用进行对话", "permission.des.read": "可使用该应用进行对话",
"permission.des.write": "可查看和编辑应用", "permission.des.write": "可查看和编辑应用",
"permission.des.log": "可查看对话日志",
"permission.name.log": "查看日志",
"plugin.Instructions": "使用说明", "plugin.Instructions": "使用说明",
"plugin_cost_by_token": "依据 token 消耗计费", "plugin_cost_by_token": "依据 token 消耗计费",
"plugin_cost_per_times": "{{cost}} 积分/次", "plugin_cost_per_times": "{{cost}} 积分/次",

View File

@ -123,6 +123,8 @@
"permission.des.manage": "在寫入權限基礎上,可以設定發布通道、檢視對話紀錄、分配這個應用程式的權限", "permission.des.manage": "在寫入權限基礎上,可以設定發布通道、檢視對話紀錄、分配這個應用程式的權限",
"permission.des.read": "可以使用這個應用程式進行對話", "permission.des.read": "可以使用這個應用程式進行對話",
"permission.des.write": "可以檢視和編輯應用程式", "permission.des.write": "可以檢視和編輯應用程式",
"permission.des.log": "可查看對話日誌",
"permission.name.log": "查看日誌",
"plugin.Instructions": "使用說明", "plugin.Instructions": "使用說明",
"plugin_cost_by_token": "根據 token 消耗計費", "plugin_cost_by_token": "根據 token 消耗計費",
"plugin_cost_per_times": "{{cost}} 積分/次", "plugin_cost_per_times": "{{cost}} 積分/次",

View File

@ -46,16 +46,22 @@ function MemberModal({
const collaboratorList = useContextSelector(CollaboratorContext, (v) => v.collaboratorList); const collaboratorList = useContextSelector(CollaboratorContext, (v) => v.collaboratorList);
const [filterClass, setFilterClass] = useState<'member' | 'org' | 'group'>(); const [filterClass, setFilterClass] = useState<'member' | 'org' | 'group'>();
const { const {
paths, paths: orgPaths,
onClickOrg, onClickOrg,
members: orgMembers, members: orgMembers,
MemberScrollData: OrgMemberScrollData, MemberScrollData: OrgMemberScrollData,
onPathClick, onPathClick: onOrgPathClick,
orgs, orgs,
searchKey, searchKey,
setSearchKey setSearchKey
} = useOrg({ withPermission: false }); } = useOrg({ withPermission: false });
const onExpandOrg = (org: OrgListItemType) => {
setFilterClass('org');
setSearchKey('');
onClickOrg(org);
};
const { const {
data: members, data: members,
ScrollData: TeamMemberScrollData, ScrollData: TeamMemberScrollData,
@ -104,8 +110,8 @@ function MemberModal({
permissionList?.read?.value permissionList?.read?.value
); );
const perLabel = useMemo(() => { const perLabel = useMemo(() => {
if (selectedPermission === undefined) return ''; if (selectedPermission === undefined) return [];
return getPerLabelList(selectedPermission!).join('、'); return getPerLabelList(selectedPermission!);
}, [getPerLabelList, selectedPermission]); }, [getPerLabelList, selectedPermission]);
const onUpdateCollaborators = useContextSelector( const onUpdateCollaborators = useContextSelector(
@ -194,6 +200,7 @@ function MemberModal({
placeholder={t('user:search_group_org_user')} placeholder={t('user:search_group_org_user')}
bgColor="myGray.50" bgColor="myGray.50"
onChange={(e) => setSearchKey(e.target.value)} onChange={(e) => setSearchKey(e.target.value)}
value={searchKey}
/> />
<Flex flexDirection="column" mt="3" overflow={'auto'} flex={'1 0 0'} h={0}> <Flex flexDirection="column" mt="3" overflow={'auto'} flex={'1 0 0'} h={0}>
@ -238,21 +245,21 @@ function MemberModal({
? t('user:team.org.org') ? t('user:team.org.org')
: t('user:team.group.group') : t('user:team.group.group')
}, },
...paths ...orgPaths
]} ]}
onClick={(parentId) => { onClick={(parentId) => {
if (parentId === '') { if (parentId === '') {
setFilterClass(undefined); setFilterClass(undefined);
onPathClick(''); onOrgPathClick('');
} else if ( } else if (
parentId === 'member' || parentId === 'member' ||
parentId === 'org' || parentId === 'org' ||
parentId === 'group' parentId === 'group'
) { ) {
setFilterClass(parentId); setFilterClass(parentId);
onPathClick(''); onOrgPathClick('');
} else { } else {
onPathClick(parentId); onOrgPathClick(parentId);
} }
}} }}
rootName={t('common:Team')} rootName={t('common:Team')}
@ -329,8 +336,8 @@ function MemberModal({
bgColor: 'myGray.200' bgColor: 'myGray.200'
}} }}
onClick={(e) => { onClick={(e) => {
onClickOrg(org); // onClickOrg(org);
// setPath(getOrgChildrenPath(org)); onExpandOrg(org);
e.stopPropagation(); e.stopPropagation();
}} }}
/> />
@ -438,7 +445,7 @@ function MemberModal({
borderRadius={'md'} borderRadius={'md'}
h={'32px'} h={'32px'}
> >
{t(perLabel as any)} {perLabel.map((item) => t(item as any)).join('、')}
<ChevronDownIcon fontSize={'md'} /> <ChevronDownIcon fontSize={'md'} />
</Flex> </Flex>
} }

View File

@ -7,7 +7,8 @@ import {
Radio, Radio,
useOutsideClick, useOutsideClick,
HStack, HStack,
MenuButton MenuButton,
Checkbox
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import React, { useMemo, useRef, useState } from 'react'; import React, { useMemo, useRef, useState } from 'react';
import MyIcon from '@fastgpt/web/components/common/Icon'; import MyIcon from '@fastgpt/web/components/common/Icon';
@ -89,19 +90,21 @@ function PermissionSelect({
return permissionList['read'].value; return permissionList['read'].value;
}, [permissionList, value]); }, [permissionList, value]);
// const selectedMultipleValues = useMemo(() => { const selectedMultipleValues = useMemo(() => {
// const per = new Permission({ per: value }); const per = new Permission({ per: value });
//
// return permissionSelectList.multipleCheckBoxList
// .filter((item) => {
// return per.checkPer(item.value);
// })
// .map((item) => item.value);
// }, [permissionSelectList.multipleCheckBoxList, value]);
const onSelectPer = (per: PermissionValueType) => { return permissionSelectList.multipleCheckBoxList
if (per === value) return; .filter((item) => {
onChange(per); return per.checkPer(item.value);
})
.map((item) => item.value);
}, [permissionSelectList.multipleCheckBoxList, value]);
const onSelectPer = (perValue: PermissionValueType) => {
if (perValue === value) return;
const per = new Permission({ per: perValue });
per.addPer(...selectedMultipleValues);
onChange(per.value);
setIsOpen(false); setIsOpen(false);
}; };
@ -184,50 +187,61 @@ function PermissionSelect({
); );
})} })}
{/* <MyDivider my={3} /> <MyDivider my={2} />
{multipleValues.length > 0 && <Box m="4"></Box>} */} {/* {permissionSelectList.multipleCheckBoxList.length > 0 && (
<Box m="4"></Box>
)} */}
{/* The list of multiple select permissions */} {permissionSelectList.multipleCheckBoxList.map((item) => {
{/* {list const isChecked = selectedMultipleValues.includes(item.value);
.filter((item) => item.type === 'multiple') const isDisabled = new Permission({ per: selectedSingleValue }).checkPer(item.value);
.map((item) => { const change = () => {
const change = () => { if (isDisabled) return;
if (checkPermission(valueState, item.value)) { const per = new Permission({ per: value });
setValueState(new Permission(valueState).remove(item.value).value); if (isChecked) {
} else { per.removePer(item.value);
setValueState(new Permission(valueState).add(item.value).value); } else {
} per.addPer(item.value);
}; }
return ( onChange(per.value);
<Flex };
key={item.value} return (
{...(checkPermission(valueState, item.value) <Flex
? { key={item.value}
color: 'primary.500', {...(isChecked
bg: 'myWhite.300' ? {
} color: 'primary.500',
: {})} bg: 'myWhite.300'
whiteSpace="pre-wrap" }
flexDirection="row" : {})}
justifyContent="start" whiteSpace="pre-wrap"
p="2" flexDirection="row"
_hover={{ justifyContent="start"
bg: 'myGray.50' p="2"
}} _hover={{
> bg: 'myGray.50'
<Checkbox }}
size="lg" {...(isDisabled
isChecked={checkPermission(valueState, item.value)} ? {
onChange={change} cursor: 'not-allowed',
/> opacity: 0.5
<Flex px="4" flexDirection="column" onClick={change}> }
<Box fontWeight="500">{item.name}</Box> : {})}
<Box fontWeight="400">{item.description}</Box> >
</Flex> <Checkbox
size="lg"
isChecked={isChecked}
onChange={change}
isDisabled={isDisabled}
/>
<Flex px="4" flexDirection="column" onClick={change}>
<Box fontWeight="500">{t(item.name as any)}</Box>
<Box fontWeight="400">{t(item.description as any)}</Box>
</Flex> </Flex>
); </Flex>
})}*/} );
})}
{onDelete && ( {onDelete && (
<> <>
<MyDivider my={2} h={'2px'} borderColor={'myGray.200'} /> <MyDivider my={2} h={'2px'} borderColor={'myGray.200'} />

View File

@ -35,10 +35,10 @@ const RouteTab = () => {
{ {
label: t('app:publish_channel'), label: t('app:publish_channel'),
id: TabEnum.publish id: TabEnum.publish
}, }
{ label: t('app:chat_logs'), id: TabEnum.logs }
] ]
: []) : []),
...(appDetail.permission.hasLogPer ? [{ label: t('app:chat_logs'), id: TabEnum.logs }] : [])
], ],
[appDetail.permission.hasManagePer, appDetail.type] [appDetail.permission.hasManagePer, appDetail.type]
); );

View File

@ -18,6 +18,7 @@ import { MongoTeamMember } from '@fastgpt/service/support/user/team/teamMemberSc
import type { ChatSourceEnum } from '@fastgpt/global/core/chat/constants'; import type { ChatSourceEnum } from '@fastgpt/global/core/chat/constants';
import { ChatItemValueTypeEnum } from '@fastgpt/global/core/chat/constants'; import { ChatItemValueTypeEnum } from '@fastgpt/global/core/chat/constants';
import { type AIChatItemValueItemType } from '@fastgpt/global/core/chat/type'; import { type AIChatItemValueItemType } from '@fastgpt/global/core/chat/type';
import { AppLogPermissionVal } from '@fastgpt/global/support/permission/app/constant';
const formatJsonString = (data: any) => { const formatJsonString = (data: any) => {
return JSON.stringify(data).replace(/"/g, '""').replace(/\n/g, '\\n'); return JSON.stringify(data).replace(/"/g, '""').replace(/\n/g, '\\n');
@ -44,7 +45,7 @@ async function handler(req: ApiRequestProps<ExportChatLogsBody, {}>, res: NextAp
throw new Error('缺少参数'); throw new Error('缺少参数');
} }
const { teamId } = await authApp({ req, authToken: true, appId, per: WritePermissionVal }); const { teamId } = await authApp({ req, authToken: true, appId, per: AppLogPermissionVal });
const teamMemberWithContact = await MongoTeamMember.aggregate([ const teamMemberWithContact = await MongoTeamMember.aggregate([
{ $match: { teamId: new Types.ObjectId(teamId) } }, { $match: { teamId: new Types.ObjectId(teamId) } },

View File

@ -13,6 +13,7 @@ import { parsePaginationRequest } from '@fastgpt/service/common/api/pagination';
import { type PaginationResponse } from '@fastgpt/web/common/fetch/type'; import { type PaginationResponse } from '@fastgpt/web/common/fetch/type';
import { addSourceMember } from '@fastgpt/service/support/user/utils'; import { addSourceMember } from '@fastgpt/service/support/user/utils';
import { replaceRegChars } from '@fastgpt/global/common/string/tools'; import { replaceRegChars } from '@fastgpt/global/common/string/tools';
import { AppLogPermissionVal } from '@fastgpt/global/support/permission/app/constant';
async function handler( async function handler(
req: NextApiRequest, req: NextApiRequest,
@ -33,7 +34,7 @@ async function handler(
} }
// 凭证校验 // 凭证校验
const { teamId } = await authApp({ req, authToken: true, appId, per: WritePermissionVal }); const { teamId } = await authApp({ req, authToken: true, appId, per: AppLogPermissionVal });
const where = { const where = {
teamId: new Types.ObjectId(teamId), teamId: new Types.ObjectId(teamId),