From 36e59cc5cbdf9e6b6bd5dec6d1203348e44bc3db Mon Sep 17 00:00:00 2001 From: chanzhi82020 Date: Thu, 4 Sep 2025 14:36:22 +0800 Subject: [PATCH] feat: add evaluation authentication --- packages/global/core/evaluation/type.d.ts | 5 + .../global/support/permission/constant.ts | 3 +- .../support/permission/evaluation/constant.ts | 21 + .../permission/evaluation/controller.ts | 23 + .../support/permission/user/constant.ts | 25 +- .../support/permission/user/controller.ts | 5 + packages/service/core/evaluation/common.ts | 560 +++++++++++++++-- .../service/core/evaluation/task/index.ts | 179 +++--- .../support/permission/evaluation/auth.ts | 355 +++++++++-- packages/web/i18n/en/account_team.json | 2 + packages/web/i18n/zh-CN/account_team.json | 2 + .../account/team/PermissionManage/index.tsx | 26 + .../app/src/pages/api/common/file/upload.ts | 6 +- .../evaluation/dataset/collection/create.ts | 18 +- .../evaluation/dataset/collection/delete.ts | 8 +- .../dataset/collection/deleteTask.ts | 12 +- .../dataset/collection/failedTasks.ts | 12 +- .../evaluation/dataset/collection/list.ts | 247 +++++--- .../collection/qualityAssessmentBatch.ts | 16 +- .../dataset/collection/retryTask.ts | 12 +- .../evaluation/dataset/collection/update.ts | 18 +- .../core/evaluation/dataset/data/create.ts | 16 +- .../core/evaluation/dataset/data/delete.ts | 18 +- .../core/evaluation/dataset/data/fileId.ts | 11 +- .../api/core/evaluation/dataset/data/list.ts | 27 +- .../dataset/data/qualityAssessment.ts | 16 +- .../evaluation/dataset/data/smartGenerate.ts | 20 +- .../core/evaluation/dataset/data/update.ts | 16 +- .../api/core/evaluation/metric/create.ts | 18 +- .../api/core/evaluation/metric/delete.ts | 12 +- .../api/core/evaluation/metric/detail.ts | 16 +- .../pages/api/core/evaluation/metric/list.ts | 114 +++- .../api/core/evaluation/metric/update.ts | 19 +- .../pages/api/core/evaluation/task/create.ts | 30 +- .../pages/api/core/evaluation/task/delete.ts | 6 +- .../pages/api/core/evaluation/task/detail.ts | 7 +- .../api/core/evaluation/task/item/delete.ts | 6 +- .../api/core/evaluation/task/item/detail.ts | 7 +- .../api/core/evaluation/task/item/export.ts | 12 +- .../api/core/evaluation/task/item/list.ts | 12 +- .../api/core/evaluation/task/item/retry.ts | 6 +- .../api/core/evaluation/task/item/update.ts | 16 +- .../pages/api/core/evaluation/task/list.ts | 93 ++- .../api/core/evaluation/task/retryFailed.ts | 6 +- .../pages/api/core/evaluation/task/start.ts | 8 +- .../pages/api/core/evaluation/task/stats.ts | 6 +- .../pages/api/core/evaluation/task/stop.ts | 6 +- .../pages/api/core/evaluation/task/update.ts | 12 +- .../components/OldEvaluationTasks.tsx | 241 -------- .../evaluation/components/create.tsx | 384 ------------ .../evaluation/dataset/detail/index.tsx | 17 - .../evaluation/dataset/fileImport.tsx | 325 ---------- .../dashboard/evaluation/dataset/index.tsx | 493 --------------- .../dashboard/evaluation/dimension/create.tsx | 128 ---- .../dashboard/evaluation/dimension/edit.tsx | 192 ------ .../dashboard/evaluation/dimension/index.tsx | 214 ------- .../src/pages/dashboard/evaluation/index.tsx | 65 -- .../evaluation/task/detail/index.tsx | 40 -- .../pages/dashboard/evaluation/task/index.tsx | 541 ----------------- .../support/permission/evaluation/.env | 31 + .../permission/evaluation/.env.example | 32 + .../support/permission/evaluation/README.md | 205 +++++++ .../support/permission/evaluation/demo.js | 267 ++++++++ .../evaluation-permissions-simple.test.ts | 269 +++++++++ .../evaluation/evaluation-permissions.test.ts | 570 ++++++++++++++++++ .../permission/evaluation/get-token-guide.md | 119 ++++ .../evaluation/run-evaluation-tests.sh | 122 ++++ .../dataset/collection/create.test.ts | 23 +- .../dataset/collection/delete.test.ts | 26 +- .../dataset/collection/list.test.ts | 144 ++++- .../dataset/collection/update.test.ts | 21 +- .../evaluation/dataset/data/create.test.ts | 57 +- .../evaluation/dataset/data/delete.test.ts | 350 +++-------- .../evaluation/dataset/data/fileId.test.ts | 55 +- .../core/evaluation/dataset/data/list.test.ts | 62 +- .../dataset/data/qualityAssessment.test.ts | 147 ++--- .../evaluation/dataset/data/update.test.ts | 95 ++- .../api/core/evaluation/metric/create.test.ts | 148 ++++- .../api/core/evaluation/metric/delete.test.ts | 109 +++- .../api/core/evaluation/metric/detail.test.ts | 63 +- .../api/core/evaluation/metric/list.test.ts | 116 +++- .../api/core/evaluation/metric/update.test.ts | 191 ++++-- .../core/evaluation/task-handler.api.test.ts | 480 --------------- .../api/core/evaluation/task/create.test.ts | 197 ++++++ .../api/core/evaluation/task/delete.test.ts | 39 ++ .../api/core/evaluation/task/detail.test.ts | 57 ++ .../api/core/evaluation/task/list.test.ts | 137 +++++ .../api/core/evaluation/task/start.test.ts | 48 ++ .../api/core/evaluation/task/stats.test.ts | 57 ++ .../api/core/evaluation/task/stop.test.ts | 39 ++ .../api/core/evaluation/task/update.test.ts | 50 ++ 91 files changed, 4789 insertions(+), 4268 deletions(-) create mode 100644 packages/global/support/permission/evaluation/constant.ts create mode 100644 packages/global/support/permission/evaluation/controller.ts delete mode 100644 projects/app/src/pages/dashboard/evaluation/components/OldEvaluationTasks.tsx delete mode 100644 projects/app/src/pages/dashboard/evaluation/components/create.tsx delete mode 100644 projects/app/src/pages/dashboard/evaluation/dataset/detail/index.tsx delete mode 100644 projects/app/src/pages/dashboard/evaluation/dataset/fileImport.tsx delete mode 100644 projects/app/src/pages/dashboard/evaluation/dataset/index.tsx delete mode 100644 projects/app/src/pages/dashboard/evaluation/dimension/create.tsx delete mode 100644 projects/app/src/pages/dashboard/evaluation/dimension/edit.tsx delete mode 100644 projects/app/src/pages/dashboard/evaluation/dimension/index.tsx delete mode 100644 projects/app/src/pages/dashboard/evaluation/index.tsx delete mode 100644 projects/app/src/pages/dashboard/evaluation/task/detail/index.tsx delete mode 100644 projects/app/src/pages/dashboard/evaluation/task/index.tsx create mode 100644 test/cases/function/packages/service/support/permission/evaluation/.env create mode 100644 test/cases/function/packages/service/support/permission/evaluation/.env.example create mode 100644 test/cases/function/packages/service/support/permission/evaluation/README.md create mode 100755 test/cases/function/packages/service/support/permission/evaluation/demo.js create mode 100644 test/cases/function/packages/service/support/permission/evaluation/evaluation-permissions-simple.test.ts create mode 100644 test/cases/function/packages/service/support/permission/evaluation/evaluation-permissions.test.ts create mode 100644 test/cases/function/packages/service/support/permission/evaluation/get-token-guide.md create mode 100755 test/cases/function/packages/service/support/permission/evaluation/run-evaluation-tests.sh delete mode 100644 test/cases/pages/api/core/evaluation/task-handler.api.test.ts create mode 100644 test/cases/pages/api/core/evaluation/task/create.test.ts create mode 100644 test/cases/pages/api/core/evaluation/task/delete.test.ts create mode 100644 test/cases/pages/api/core/evaluation/task/detail.test.ts create mode 100644 test/cases/pages/api/core/evaluation/task/list.test.ts create mode 100644 test/cases/pages/api/core/evaluation/task/start.test.ts create mode 100644 test/cases/pages/api/core/evaluation/task/stats.test.ts create mode 100644 test/cases/pages/api/core/evaluation/task/stop.test.ts create mode 100644 test/cases/pages/api/core/evaluation/task/update.test.ts diff --git a/packages/global/core/evaluation/type.d.ts b/packages/global/core/evaluation/type.d.ts index 81900b22ea82..e11f641fd3ae 100644 --- a/packages/global/core/evaluation/type.d.ts +++ b/packages/global/core/evaluation/type.d.ts @@ -1,6 +1,7 @@ import type { EvaluationStatusEnum } from './constants'; import type { EvalDatasetDataSchemaType } from './dataset/type'; import type { MetricResult } from './metric/type'; +import type { EvaluationPermission } from '../../../support/permission/evaluation/controller'; // Evaluation target related types export interface WorkflowConfig { @@ -122,3 +123,7 @@ export interface EvaluationItemJobData { evalId: string; evalItemId: string; } + +export type EvaluationDetailType = EvaluationSchemaType & { + permission: EvaluationPermission; +}; diff --git a/packages/global/support/permission/constant.ts b/packages/global/support/permission/constant.ts index de254f89b487..1c59c565f0b3 100644 --- a/packages/global/support/permission/constant.ts +++ b/packages/global/support/permission/constant.ts @@ -49,7 +49,8 @@ export const PermissionTypeMap = { export enum PerResourceTypeEnum { team = 'team', app = 'app', - dataset = 'dataset' + dataset = 'dataset', + evaluation = 'evaluation' } /* new permission */ diff --git a/packages/global/support/permission/evaluation/constant.ts b/packages/global/support/permission/evaluation/constant.ts new file mode 100644 index 000000000000..ddcd06a24868 --- /dev/null +++ b/packages/global/support/permission/evaluation/constant.ts @@ -0,0 +1,21 @@ +import { type PermissionListType, type RoleListType, type RolePerMapType } from '../type'; +import { CommonPerList, CommonRoleList, CommonRolePerMap } from '../constant'; + +// 评估模块权限列表 (沿用通用权限,无特殊权限) +export const EvaluationPerList: PermissionListType = CommonPerList; + +// 评估模块角色列表 (沿用通用角色) +export const EvaluationRoleList: RoleListType = { + ...CommonRoleList +} as const; + +// 评估模块角色权限映射 (沿用通用映射) +export const EvaluationRolePerMap: RolePerMapType = CommonRolePerMap; + +// 评估模块默认权限值 +export const EvaluationDefaultRoleVal = 0; + +// 常用权限值导出 +export const EvaluationReadPermissionVal = EvaluationPerList.read; +export const EvaluationWritePermissionVal = EvaluationPerList.write; +export const EvaluationManagePermissionVal = EvaluationPerList.manage; diff --git a/packages/global/support/permission/evaluation/controller.ts b/packages/global/support/permission/evaluation/controller.ts new file mode 100644 index 000000000000..5e8ed700702e --- /dev/null +++ b/packages/global/support/permission/evaluation/controller.ts @@ -0,0 +1,23 @@ +import { type PerConstructPros, Permission } from '../controller'; +import { + EvaluationDefaultRoleVal, + EvaluationPerList, + EvaluationRoleList, + EvaluationRolePerMap +} from './constant'; + +export class EvaluationPermission extends Permission { + constructor(props?: PerConstructPros) { + if (!props) { + props = { + role: EvaluationDefaultRoleVal + }; + } else if (!props?.role) { + props.role = EvaluationDefaultRoleVal; + } + props.roleList = EvaluationRoleList; + props.rolePerMap = EvaluationRolePerMap; + props.perList = EvaluationPerList; + super(props); + } +} diff --git a/packages/global/support/permission/user/constant.ts b/packages/global/support/permission/user/constant.ts index 9a99d873bc66..b22cd0351428 100644 --- a/packages/global/support/permission/user/constant.ts +++ b/packages/global/support/permission/user/constant.ts @@ -12,20 +12,23 @@ import { sumPer } from '../utils'; export enum TeamPerKeyEnum { appCreate = 'appCreate', datasetCreate = 'datasetCreate', - apikeyCreate = 'apikeyCreate' + apikeyCreate = 'apikeyCreate', + evaluationCreate = 'evaluationCreate' } export enum TeamRoleKeyEnum { appCreate = 'appCreate', datasetCreate = 'datasetCreate', - apikeyCreate = 'apikeyCreate' + apikeyCreate = 'apikeyCreate', + evaluationCreate = 'evaluationCreate' } export const TeamPerList: PermissionListType = { ...CommonPerList, apikeyCreate: 0b100000, appCreate: 0b001000, - datasetCreate: 0b010000 + datasetCreate: 0b010000, + evaluationCreate: 0b1000000 }; export const TeamRoleList: RoleListType = { @@ -58,6 +61,12 @@ export const TeamRoleList: RoleListType = { description: '', name: i18nT('account_team:permission_apikeyCreate'), value: 0b100000 + }, + [TeamRoleKeyEnum.evaluationCreate]: { + checkBoxType: 'multiple', + description: '', + name: i18nT('account_team:permission_evaluationCreate'), + value: 0b1000000 } }; @@ -78,6 +87,14 @@ export const TeamRolePerMap: RolePerMapType = new Map([ [ TeamRoleList['apikeyCreate'].value, sumPer(TeamPerList.apikeyCreate, CommonPerList.read, CommonPerList.write) as PermissionValueType + ], + [ + TeamRoleList['evaluationCreate'].value, + sumPer( + TeamPerList.evaluationCreate, + CommonPerList.read, + CommonPerList.write + ) as PermissionValueType ] ]); @@ -85,6 +102,7 @@ export const TeamReadRoleVal = TeamRoleList['read'].value; export const TeamWriteRoleVal = TeamRoleList['write'].value; export const TeamManageRoleVal = TeamRoleList['manage'].value; export const TeamAppCreateRoleVal = TeamRoleList['appCreate'].value; +export const TeamEvaluationCreateRoleVal = TeamRoleList['evaluationCreate'].value; export const TeamDatasetCreateRoleVal = TeamRoleList['datasetCreate'].value; export const TeamApikeyCreateRoleVal = TeamRoleList['apikeyCreate'].value; export const TeamDefaultRoleVal = TeamReadRoleVal; @@ -95,4 +113,5 @@ export const TeamManagePermissionVal = TeamPerList.manage; export const TeamAppCreatePermissionVal = TeamPerList.appCreate; export const TeamDatasetCreatePermissionVal = TeamPerList.datasetCreate; export const TeamApikeyCreatePermissionVal = TeamPerList.apikeyCreate; +export const TeamEvaluationCreatePermissionVal = TeamPerList.evaluationCreate; export const TeamDefaultPermissionVal = TeamReadPermissionVal; diff --git a/packages/global/support/permission/user/controller.ts b/packages/global/support/permission/user/controller.ts index b92f98072da8..2abdfc2e10d1 100644 --- a/packages/global/support/permission/user/controller.ts +++ b/packages/global/support/permission/user/controller.ts @@ -3,6 +3,7 @@ import { TeamApikeyCreateRoleVal, TeamAppCreateRoleVal, TeamDatasetCreateRoleVal, + TeamEvaluationCreateRoleVal, TeamDefaultRoleVal, TeamPerList, TeamRoleList, @@ -13,9 +14,11 @@ export class TeamPermission extends Permission { hasAppCreateRole: boolean = false; hasDatasetCreateRole: boolean = false; hasApikeyCreateRole: boolean = false; + hasEvaluationCreateRole: boolean = false; hasAppCreatePer: boolean = false; hasDatasetCreatePer: boolean = false; hasApikeyCreatePer: boolean = false; + hasEvaluationCreatePer: boolean = false; constructor(props?: PerConstructPros) { if (!props) { @@ -34,9 +37,11 @@ export class TeamPermission extends Permission { this.hasAppCreateRole = this.checkRole(TeamAppCreateRoleVal); this.hasDatasetCreateRole = this.checkRole(TeamDatasetCreateRoleVal); this.hasApikeyCreateRole = this.checkRole(TeamApikeyCreateRoleVal); + this.hasEvaluationCreateRole = this.checkRole(TeamEvaluationCreateRoleVal); this.hasAppCreatePer = this.checkPer(TeamAppCreateRoleVal); this.hasDatasetCreatePer = this.checkPer(TeamDatasetCreateRoleVal); this.hasApikeyCreatePer = this.checkPer(TeamApikeyCreateRoleVal); + this.hasEvaluationCreatePer = this.checkPer(TeamEvaluationCreateRoleVal); }); } } diff --git a/packages/service/core/evaluation/common.ts b/packages/service/core/evaluation/common.ts index 9db4b5843a11..ca34c44dec88 100644 --- a/packages/service/core/evaluation/common.ts +++ b/packages/service/core/evaluation/common.ts @@ -1,40 +1,15 @@ import { Types } from 'mongoose'; -import { parseHeaderCert } from '../../support/permission/controller'; import type { AuthModeType } from '../../support/permission/type'; -export const validateResourceAccess = async ( - resourceId: string, - auth: AuthModeType, - resourceName: string = 'Resource' -) => { - const { teamId } = await parseHeaderCert(auth); - return { - teamId, - resourceFilter: { - _id: new Types.ObjectId(resourceId), - teamId - }, - notFoundError: `${resourceName} not found` - }; -}; -export const validateResourcesAccess = async ( - resourceIds: string[], - auth: AuthModeType, - resourceName: string = 'Resource' -) => { - const { teamId } = await parseHeaderCert(auth); - return { - teamId, - resourceFilter: { - _id: { $in: resourceIds.map((id) => new Types.ObjectId(id)) }, - teamId - }, - notFoundError: `${resourceName} not found` - }; -}; -export const validateResourceCreate = async (auth: AuthModeType) => { - const { teamId, tmbId } = await parseHeaderCert(auth); - return { teamId, tmbId }; -}; +import { authEvaluation } from '../../support/permission/evaluation/auth'; +import { authEvalDataset, authEvalMetric } from '../../support/permission/evaluation/auth'; +import { authUserPer } from '../../support/permission/user/auth'; +import { TeamEvaluationCreatePermissionVal } from '@fastgpt/global/support/permission/user/constant'; +import { ReadPermissionVal, WritePermissionVal } from '@fastgpt/global/support/permission/constant'; +import type { EvalTarget, EvaluationDetailType } from '@fastgpt/global/core/evaluation/type'; +import { authApp } from '../../support/permission/app/auth'; +import { authDatasetCollection } from '../../support/permission/dataset/auth'; + +// Generic validation functions removed - replaced with resource-specific functions below export const buildListQuery = ( teamId: string, searchKey?: string, @@ -50,18 +25,7 @@ export const buildListQuery = ( return filter; }; -export const validateListAccess = async ( - auth: AuthModeType, - searchKey?: string, - page: number = 1, - pageSize: number = 20 -) => { - const { teamId } = await parseHeaderCert(auth); - const filter = buildListQuery(teamId, searchKey); - const { skip, limit, sort } = buildPaginationOptions(page, pageSize); - - return { teamId, filter, skip, limit, sort }; -}; +// Generic list validation removed - replaced with resource-specific functions below export const buildPaginationOptions = (page: number = 1, pageSize: number = 20) => ({ skip: (page - 1) * pageSize, limit: pageSize, @@ -78,3 +42,505 @@ export const checkDeleteResult = (result: any, resourceName: string = 'Resource' throw new Error(`${resourceName} not found`); } }; + +/** + * 获取用户的评估权限聚合信息(用于列表查询) + */ +export const getEvaluationPermissionAggregation = async ( + auth: AuthModeType +): Promise<{ + teamId: string; + tmbId: string; + isOwner: boolean; + roleList: any[]; + myGroupMap: Map; + myOrgSet: Set; +}> => { + const { authUserPer } = await import('../../support/permission/user/auth'); + const { PerResourceTypeEnum, ReadPermissionVal } = await import( + '@fastgpt/global/support/permission/constant' + ); + const { MongoResourcePermission } = await import('../../support/permission/schema'); + const { getGroupsByTmbId } = await import('../../support/permission/memberGroup/controllers'); + const { getOrgIdSetWithParentByTmbId } = await import('../../support/permission/org/controllers'); + + // Auth user permission - 支持API Key和Token认证 + const { + tmbId, + teamId, + permission: teamPer + } = await authUserPer({ + ...auth, + per: ReadPermissionVal + }); + + // Get team all evaluation permissions + const [roleList, myGroupMap, myOrgSet] = await Promise.all([ + MongoResourcePermission.find({ + resourceType: PerResourceTypeEnum.evaluation, + teamId, + resourceId: { + $exists: true + } + }).lean(), + getGroupsByTmbId({ + tmbId, + teamId + }).then((item) => { + const map = new Map(); + item.forEach((item) => { + map.set(String(item._id), 1); + }); + return map; + }), + getOrgIdSetWithParentByTmbId({ + teamId, + tmbId + }) + ]); + + return { + teamId, + tmbId, + isOwner: teamPer.isOwner, + roleList, + myGroupMap, + myOrgSet + }; +}; + +// ================ 评估模块专用权限验证函数 ================ + +/** + * 验证评估任务创建权限 + * 包含: 团队创建权限 + target关联APP读权限 + */ +export const authEvaluationTaskCreate = async ( + target: EvalTarget, + auth: AuthModeType +): Promise<{ + teamId: string; + tmbId: string; +}> => { + const { teamId, tmbId } = await authUserPer({ + ...auth, + per: TeamEvaluationCreatePermissionVal + }); + + if (target.type == 'workflow') { + if (!target.config?.appId) { + return Promise.reject('Invalid target configuration: missing appId'); + } + await authApp({ + ...auth, + appId: target.config.appId, + per: ReadPermissionVal // APP需要读权限才能被评估调用 + }); + } + + return { + teamId, + tmbId + }; +}; + +/** + * 验证评估任务读取权限 + */ +export const authEvaluationTaskRead = async ( + evaluationId: string, + auth: AuthModeType +): Promise<{ + evaluation: EvaluationDetailType; + teamId: string; + tmbId: string; +}> => { + const { evaluation, teamId, tmbId } = await authEvaluation({ + ...auth, + evaluationId, + per: ReadPermissionVal + }); + + return { evaluation, teamId, tmbId }; +}; + +/** + * 验证评估任务写入权限 + */ +export const authEvaluationTaskWrite = async ( + evaluationId: string, + auth: AuthModeType +): Promise<{ + evaluation: EvaluationDetailType; + teamId: string; + tmbId: string; +}> => { + const { evaluation, teamId, tmbId } = await authEvaluation({ + ...auth, + evaluationId, + per: WritePermissionVal + }); + + return { evaluation, teamId, tmbId }; +}; + +/** + * 验证评估任务执行权限 + * 包含: 评估写权限 + target关联APP读权限 + */ +export const authEvaluationTaskExecution = async ( + evaluationId: string, + auth: AuthModeType +): Promise<{ + teamId: string; + tmbId: string; +}> => { + // 验证评估任务的写权限并获取详情 + const { evaluation, teamId, tmbId } = await authEvaluation({ + ...auth, + evaluationId, + per: WritePermissionVal + }); + + // 验证target关联APP的读权限 + if (evaluation.target.type == 'workflow') { + if (!evaluation.target.config?.appId) { + return Promise.reject('Invalid target configuration: missing appId'); + } + await authApp({ + ...auth, + appId: evaluation.target.config.appId, + per: ReadPermissionVal // APP需要读权限才能被评估调用 + }); + } + + return { + teamId, + tmbId + }; +}; + +// ================ 评估项目(EvaluationItem)专用权限验证函数 ================ + +/** + * 验证评估项目读取权限 + */ +export const authEvaluationItemRead = async ( + evalItemId: string, + auth: AuthModeType +): Promise<{ + teamId: string; + tmbId: string; + evalItemId: string; + evalId: string; +}> => { + const { MongoEvalItem } = await import('./task/schema'); + + // 根据evalItemId获取evalId + const evalItem = await MongoEvalItem.findById(evalItemId).select('evalId').lean(); + if (!evalItem) { + throw new Error('Evaluation item not found'); + } + + // 验证评估任务的读权限 + const { teamId, tmbId } = await authEvaluationTaskRead(evalItem.evalId, auth); + + return { teamId, tmbId, evalItemId, evalId: evalItem.evalId }; +}; + +/** + * 验证评估项目写入权限 + */ +export const authEvaluationItemWrite = async ( + evalItemId: string, + auth: AuthModeType +): Promise<{ + teamId: string; + tmbId: string; + evalItemId: string; + evalId: string; +}> => { + const { MongoEvalItem } = await import('./task/schema'); + + // 根据evalItemId获取evalId + const evalItem = await MongoEvalItem.findById(evalItemId).select('evalId').lean(); + if (!evalItem) { + throw new Error('Evaluation item not found'); + } + + // 验证评估任务的写权限 + const { teamId, tmbId } = await authEvaluationTaskWrite(evalItem.evalId, auth); + + return { teamId, tmbId, evalItemId, evalId: evalItem.evalId }; +}; + +/** + * 验证评估项目重试权限 + */ +export const authEvaluationItemRetry = async ( + evalItemId: string, + auth: AuthModeType +): Promise<{ + teamId: string; + tmbId: string; + evalItemId: string; + evalId: string; +}> => { + // 重试权限等同于写入权限 + return await authEvaluationItemWrite(evalItemId, auth); +}; + +// ================ 评估数据集(EvaluationDataset)专用权限验证函数 ================ + +/** + * 验证评估数据集创建权限 + */ +export const authEvaluationDatasetCreate = async ( + auth: AuthModeType +): Promise<{ + teamId: string; + tmbId: string; +}> => { + // 评估数据集创建需要团队评估创建权限 + const { teamId, tmbId } = await authUserPer({ + ...auth, + per: TeamEvaluationCreatePermissionVal + }); + + return { teamId, tmbId }; +}; + +/** + * 验证评估数据集读取权限 + */ +export const authEvaluationDatasetRead = async ( + datasetId: string, + auth: AuthModeType +): Promise<{ + teamId: string; + tmbId: string; + datasetId: string; +}> => { + const { teamId, tmbId } = await authEvalDataset({ + ...auth, + datasetId, + per: ReadPermissionVal + }); + + return { teamId, tmbId, datasetId }; +}; + +/** + * 验证评估数据集写入权限 + */ +export const authEvaluationDatasetWrite = async ( + datasetId: string, + auth: AuthModeType +): Promise<{ + teamId: string; + tmbId: string; + datasetId: string; +}> => { + const { teamId, tmbId } = await authEvalDataset({ + ...auth, + datasetId, + per: WritePermissionVal + }); + + return { teamId, tmbId, datasetId }; +}; + +/** + * 验证从知识库生成评估数据集的权限 + */ +export const authEvaluationDatasetGenFromKnowledgeBase = async ( + datasetId: string, + kbCollectionIds: string[], + auth: AuthModeType +): Promise<{ + teamId: string; + tmbId: string; +}> => { + const { teamId, tmbId } = await authEvaluationDatasetRead(datasetId, auth); + + // 验证知识库的读权限 + await Promise.all( + kbCollectionIds.map((collectionId) => + authDatasetCollection({ + ...auth, + collectionId, + per: ReadPermissionVal + }) + ) + ); + + return { + teamId, + tmbId + }; +}; + +// ================ 评估指标(EvaluationMetric)专用权限验证函数 ================ + +/** + * 验证评估指标创建权限 + */ +export const authEvaluationMetricCreate = async ( + auth: AuthModeType +): Promise<{ + teamId: string; + tmbId: string; +}> => { + // 评估指标创建需要团队评估创建权限 + const { teamId, tmbId } = await authUserPer({ + ...auth, + per: TeamEvaluationCreatePermissionVal + }); + + return { teamId, tmbId }; +}; + +/** + * 验证评估指标读取权限 + */ +export const authEvaluationMetricRead = async ( + metricId: string, + auth: AuthModeType +): Promise<{ + teamId: string; + tmbId: string; + metricId: string; +}> => { + const { teamId, tmbId } = await authEvalMetric({ + ...auth, + metricId, + per: ReadPermissionVal + }); + + return { teamId, tmbId, metricId }; +}; + +/** + * 验证评估指标写入权限 + */ +export const authEvaluationMetricWrite = async ( + metricId: string, + auth: AuthModeType +): Promise<{ + teamId: string; + tmbId: string; + metricId: string; +}> => { + const { teamId, tmbId } = await authEvalMetric({ + ...auth, + metricId, + per: WritePermissionVal + }); + + return { teamId, tmbId, metricId }; +}; + +// ================ 评估数据集数据(EvaluationDatasetData)专用权限验证函数 ================ + +/** + * 验证评估数据集数据读取权限 + */ +export const authEvaluationDatasetDataRead = async ( + collectionId: string, + auth: AuthModeType +): Promise<{ + teamId: string; + tmbId: string; + collectionId: string; +}> => { + // 数据读取需要数据集的读权限 + const { teamId, tmbId } = await authEvaluationDatasetRead(collectionId, auth); + + return { teamId, tmbId, collectionId }; +}; + +/** + * 验证评估数据集数据写入权限 + */ +export const authEvaluationDatasetDataWrite = async ( + collectionId: string, + auth: AuthModeType +): Promise<{ + teamId: string; + tmbId: string; + collectionId: string; +}> => { + // 数据写入需要数据集的写权限 + const { teamId, tmbId } = await authEvaluationDatasetWrite(collectionId, auth); + + return { teamId, tmbId, collectionId }; +}; + +/** + * 验证评估数据集数据创建权限 + */ +export const authEvaluationDatasetDataCreate = async ( + collectionId: string, + auth: AuthModeType +): Promise<{ + teamId: string; + tmbId: string; + collectionId: string; +}> => { + // 数据创建需要数据集的写权限 + return await authEvaluationDatasetDataWrite(collectionId, auth); +}; + +/** + * 验证评估数据集数据删除权限 + */ +export const authEvaluationDatasetDataDelete = async ( + collectionId: string, + auth: AuthModeType +): Promise<{ + teamId: string; + tmbId: string; + collectionId: string; +}> => { + // 数据删除需要数据集的写权限 + return await authEvaluationDatasetDataWrite(collectionId, auth); +}; + +/** + * 验证评估数据集数据更新权限 + */ +export const authEvaluationDatasetDataUpdate = async ( + collectionId: string, + auth: AuthModeType +): Promise<{ + teamId: string; + tmbId: string; + collectionId: string; +}> => { + // 数据更新需要数据集的写权限 + return await authEvaluationDatasetDataWrite(collectionId, auth); +}; + +/** + * 通过数据项ID验证评估数据集数据更新权限 + */ +export const authEvaluationDatasetDataUpdateById = async ( + dataId: string, + auth: AuthModeType +): Promise<{ + teamId: string; + tmbId: string; + collectionId: string; +}> => { + const { MongoEvalDatasetData } = await import('./dataset/evalDatasetDataSchema'); + + // 根据dataId获取collectionId + const dataItem = await MongoEvalDatasetData.findById(dataId).select('datasetId').lean(); + if (!dataItem) { + throw new Error('Dataset data not found'); + } + + // 使用collectionId进行权限验证 + const collectionId = String(dataItem.datasetId); + return await authEvaluationDatasetDataUpdate(collectionId, auth); +}; diff --git a/packages/service/core/evaluation/task/index.ts b/packages/service/core/evaluation/task/index.ts index 9c69719ed9ff..56bd0c9510c6 100644 --- a/packages/service/core/evaluation/task/index.ts +++ b/packages/service/core/evaluation/task/index.ts @@ -3,17 +3,10 @@ import type { EvaluationSchemaType, EvaluationItemSchemaType, CreateEvaluationParams, - EvaluationDisplayType, EvaluationItemDisplayType } from '@fastgpt/global/core/evaluation/type'; -import type { AuthModeType } from '../../../support/permission/type'; -import { - validateResourceAccess, - validateResourceCreate, - validateListAccess, - checkUpdateResult, - checkDeleteResult -} from '../common'; +import { checkUpdateResult, checkDeleteResult } from '../common'; +import { Types } from 'mongoose'; import { EvaluationStatusEnum } from '@fastgpt/global/core/evaluation/constants'; import { evaluationTaskQueue, @@ -28,10 +21,12 @@ import { checkTeamAIPoints } from '../../../support/permission/teamLimit'; export class EvaluationTaskService { static async createEvaluation( - params: CreateEvaluationParams, - auth: AuthModeType + params: CreateEvaluationParams & { + teamId: string; + tmbId: string; + } ): Promise { - const { teamId, tmbId } = await validateResourceCreate(auth); + const { teamId, tmbId, ...evaluationParams } = params; // Check AI Points balance await checkTeamAIPoints(teamId); @@ -40,12 +35,12 @@ export class EvaluationTaskService { const { billId } = await createTrainingUsage({ teamId, tmbId, - appName: params.name, + appName: evaluationParams.name, billSource: UsageSourceEnum.evaluation }); const evaluation = await MongoEvaluation.create({ - ...params, + ...evaluationParams, teamId, tmbId, usageId: billId, @@ -56,44 +51,38 @@ export class EvaluationTaskService { return evaluation.toObject(); } - static async getEvaluation(evalId: string, auth: AuthModeType): Promise { - const { resourceFilter, notFoundError } = await validateResourceAccess( - evalId, - auth, - 'Evaluation' - ); - - const evaluation = await MongoEvaluation.findOne(resourceFilter).lean(); - + static async getEvaluation(evalId: string): Promise { + const evaluation = await MongoEvaluation.findOne({ _id: new Types.ObjectId(evalId) }).lean(); if (!evaluation) { - throw new Error(notFoundError); + throw new Error('Evaluation not found'); } - return evaluation; } static async updateEvaluation( evalId: string, updates: Partial, - auth: AuthModeType + teamId: string ): Promise { - const { resourceFilter } = await validateResourceAccess(evalId, auth, 'Evaluation'); - - const result = await MongoEvaluation.updateOne(resourceFilter, { $set: updates }); + const result = await MongoEvaluation.updateOne( + { _id: new Types.ObjectId(evalId), teamId: new Types.ObjectId(teamId) }, + { $set: updates } + ); checkUpdateResult(result, 'Evaluation'); } - static async deleteEvaluation(evalId: string, auth: AuthModeType): Promise { - const { resourceFilter } = await validateResourceAccess(evalId, auth, 'Evaluation'); - + static async deleteEvaluation(evalId: string, teamId: string): Promise { // Remove related tasks from queue to prevent further processing await Promise.all([removeEvaluationTaskJob(evalId), removeEvaluationItemJobs(evalId)]); // Delete all evaluation items for this evaluation task await MongoEvalItem.deleteMany({ evalId: evalId }); - const result = await MongoEvaluation.deleteOne(resourceFilter); + const result = await MongoEvaluation.deleteOne({ + _id: new Types.ObjectId(evalId), + teamId: new Types.ObjectId(teamId) + }); checkDeleteResult(result, 'Evaluation'); @@ -101,19 +90,44 @@ export class EvaluationTaskService { } static async listEvaluations( - auth: AuthModeType, + teamId: string, page: number = 1, pageSize: number = 20, - searchKey?: string + searchKey?: string, + accessibleIds?: string[], + tmbId?: string, + isOwner: boolean = false ): Promise<{ - evaluations: EvaluationDisplayType[]; + list: any[]; total: number; }> { - const { filter, skip, limit, sort } = await validateListAccess(auth, searchKey, page, pageSize); + // Build basic filter and pagination + const filter: any = { teamId: new Types.ObjectId(teamId) }; + if (searchKey) { + filter.$or = [ + { name: { $regex: searchKey, $options: 'i' } }, + { description: { $regex: searchKey, $options: 'i' } } + ]; + } + const skip = (page - 1) * pageSize; + const limit = pageSize; + const sort = { createTime: -1 as const }; + + // If not owner, filter by accessible resources + let finalFilter = filter; + if (!isOwner && accessibleIds) { + finalFilter = { + ...filter, + $or: [ + { _id: { $in: accessibleIds.map((id) => new Types.ObjectId(id)) } }, + ...(tmbId ? [{ tmbId: new Types.ObjectId(tmbId) }] : []) // Own evaluations + ] + }; + } const [evaluations, total] = await Promise.all([ MongoEvaluation.aggregate([ - { $match: filter }, + { $match: finalFilter }, { $lookup: { from: 'eval_datasets', @@ -196,29 +210,37 @@ export class EvaluationTaskService { executorAvatar: 1, totalCount: 1, completedCount: 1, - errorCount: 1 + errorCount: 1, + tmbId: 1 } }, { $sort: sort }, { $skip: skip }, { $limit: limit } ]), - MongoEvaluation.countDocuments(filter) + MongoEvaluation.countDocuments(finalFilter) ]); - return { evaluations, total }; + // Return raw data - permissions will be handled in API layer + return { + list: evaluations, + total + }; } static async listEvaluationItems( evalId: string, - auth: AuthModeType, + teamId: string, page: number = 1, pageSize: number = 20 ): Promise<{ items: EvaluationItemDisplayType[]; total: number; }> { - await this.getEvaluation(evalId, auth); + const evaluation = await this.getEvaluation(evalId); + if (String(evaluation.teamId) !== teamId) { + throw new Error('Evaluation not found'); + } const skip = (page - 1) * pageSize; const limit = pageSize; @@ -241,8 +263,11 @@ export class EvaluationTaskService { return { items, total }; } - static async startEvaluation(evalId: string, auth: AuthModeType): Promise { - const evaluation = await this.getEvaluation(evalId, auth); + static async startEvaluation(evalId: string, teamId: string): Promise { + const evaluation = await this.getEvaluation(evalId); + if (String(evaluation.teamId) !== teamId) { + throw new Error('Evaluation not found'); + } if (evaluation.status !== EvaluationStatusEnum.queuing) { throw new Error('Only queuing evaluations can be started'); @@ -250,7 +275,7 @@ export class EvaluationTaskService { // Update status to processing await MongoEvaluation.updateOne( - { _id: evalId }, + { _id: new Types.ObjectId(evalId) }, { $set: { status: EvaluationStatusEnum.evaluating } } ); @@ -262,8 +287,11 @@ export class EvaluationTaskService { addLog.info(`[Evaluation] Task submitted to queue: ${evalId}`); } - static async stopEvaluation(evalId: string, auth: AuthModeType): Promise { - const evaluation = await this.getEvaluation(evalId, auth); + static async stopEvaluation(evalId: string, teamId: string): Promise { + const evaluation = await this.getEvaluation(evalId); + if (String(evaluation.teamId) !== teamId) { + throw new Error('Evaluation not found'); + } if ( ![EvaluationStatusEnum.evaluating, EvaluationStatusEnum.queuing].includes(evaluation.status) @@ -276,7 +304,7 @@ export class EvaluationTaskService { // Update status to error (manually stopped) await MongoEvaluation.updateOne( - { _id: evalId }, + { _id: new Types.ObjectId(evalId) }, { $set: { status: EvaluationStatusEnum.error, @@ -289,7 +317,7 @@ export class EvaluationTaskService { // Stop all related evaluation items await MongoEvalItem.updateMany( { - evalId: evalId, + evalId: new Types.ObjectId(evalId), status: { $in: [EvaluationStatusEnum.queuing, EvaluationStatusEnum.evaluating] } }, { @@ -306,7 +334,7 @@ export class EvaluationTaskService { static async getEvaluationStats( evalId: string, - auth: AuthModeType + teamId: string ): Promise<{ total: number; completed: number; @@ -315,7 +343,10 @@ export class EvaluationTaskService { error: number; avgScore?: number; }> { - await this.getEvaluation(evalId, auth); + const evaluation = await this.getEvaluation(evalId); + if (String(evaluation.teamId) !== teamId) { + throw new Error('Evaluation not found'); + } const stats = await MongoEvalItem.aggregate([ { $match: { evalId: evalId } }, @@ -368,7 +399,7 @@ export class EvaluationTaskService { static async getEvaluationItem( itemId: string, - auth: AuthModeType + teamId: string ): Promise { const item = await MongoEvalItem.findById(itemId).lean(); @@ -377,7 +408,10 @@ export class EvaluationTaskService { } // Validate access permission for evaluation task - await this.getEvaluation(item.evalId, auth); + const evaluation = await this.getEvaluation(item.evalId); + if (String(evaluation.teamId) !== teamId) { + throw new Error('Evaluation not found'); + } return item; } @@ -385,25 +419,25 @@ export class EvaluationTaskService { static async updateEvaluationItem( itemId: string, updates: Partial, - auth: AuthModeType + teamId: string ): Promise { - await this.getEvaluationItem(itemId, auth); + await this.getEvaluationItem(itemId, teamId); const result = await MongoEvalItem.updateOne({ _id: itemId }, { $set: updates }); checkUpdateResult(result, 'Evaluation item'); } - static async deleteEvaluationItem(itemId: string, auth: AuthModeType): Promise { - await this.getEvaluationItem(itemId, auth); + static async deleteEvaluationItem(itemId: string, teamId: string): Promise { + await this.getEvaluationItem(itemId, teamId); const result = await MongoEvalItem.deleteOne({ _id: itemId }); checkDeleteResult(result, 'Evaluation item'); } - static async retryEvaluationItem(itemId: string, auth: AuthModeType): Promise { - const item = await this.getEvaluationItem(itemId, auth); + static async retryEvaluationItem(itemId: string, teamId: string): Promise { + const item = await this.getEvaluationItem(itemId, teamId); // Only completed evaluation items without errors cannot be retried if (item.status === EvaluationStatusEnum.completed && !item.errorMessage) { @@ -439,8 +473,11 @@ export class EvaluationTaskService { addLog.info(`[Evaluation] Evaluation item reset to queuing status and resubmitted: ${itemId}`); } - static async retryFailedItems(evalId: string, auth: AuthModeType): Promise { - await this.getEvaluation(evalId, auth); + static async retryFailedItems(evalId: string, teamId: string): Promise { + const evaluation = await this.getEvaluation(evalId); + if (String(evaluation.teamId) !== teamId) { + throw new Error('Evaluation not found'); + } // Find items that need to be retried const itemsToRetry = await MongoEvalItem.find( @@ -502,7 +539,7 @@ export class EvaluationTaskService { static async getEvaluationItemResult( itemId: string, - auth: AuthModeType + teamId: string ): Promise<{ item: EvaluationItemSchemaType; dataItem: any; @@ -510,7 +547,7 @@ export class EvaluationTaskService { result?: any; score?: number; }> { - const item = await this.getEvaluationItem(itemId, auth); + const item = await this.getEvaluationItem(itemId, teamId); return { item, @@ -524,7 +561,7 @@ export class EvaluationTaskService { // Search evaluation items static async searchEvaluationItems( evalId: string, - auth: AuthModeType, + teamId: string, options: { status?: EvaluationStatusEnum; hasError?: boolean; @@ -537,7 +574,10 @@ export class EvaluationTaskService { items: EvaluationItemDisplayType[]; total: number; }> { - await this.getEvaluation(evalId, auth); + const evaluation = await this.getEvaluation(evalId); + if (String(evaluation.teamId) !== teamId) { + throw new Error('Evaluation not found'); + } const { status, hasError, scoreRange, keyword, page = 1, pageSize = 20 } = options; @@ -598,10 +638,13 @@ export class EvaluationTaskService { // Export evaluation item results static async exportEvaluationResults( evalId: string, - auth: AuthModeType, + teamId: string, format: 'csv' | 'json' = 'json' ): Promise { - await this.getEvaluation(evalId, auth); + const evaluation = await this.getEvaluation(evalId); + if (String(evaluation.teamId) !== teamId) { + throw new Error('Evaluation not found'); + } const items = await MongoEvalItem.find({ evalId: evalId }).sort({ createTime: 1 }).lean(); diff --git a/packages/service/support/permission/evaluation/auth.ts b/packages/service/support/permission/evaluation/auth.ts index 0e8e48a5b4cb..89791e30927a 100644 --- a/packages/service/support/permission/evaluation/auth.ts +++ b/packages/service/support/permission/evaluation/auth.ts @@ -1,86 +1,241 @@ +/* Auth evaluation permission */ import { parseHeaderCert } from '../controller'; -import { OwnerPermissionVal, ReadRoleVal } from '@fastgpt/global/support/permission/constant'; -import type { EvalDatasetCollectionSchemaType } from '@fastgpt/global/core/evaluation/dataset/type'; -import type { AuthModeType, AuthResponseType } from '../type'; -import { MongoEvalDatasetCollection } from '../../../core/evaluation/dataset/evalDatasetCollectionSchema'; +import { getResourcePermission } from '../controller'; +import { + OwnerPermissionVal, + PerResourceTypeEnum, + ReadPermissionVal, + ReadRoleVal +} from '@fastgpt/global/support/permission/constant'; import { getTmbInfoByTmbId } from '../../user/team/controller'; +import type { EvaluationDetailType } from '@fastgpt/global/core/evaluation/type'; +import type { AuthModeType, AuthResponseType } from '../type'; import type { PermissionValueType } from '@fastgpt/global/support/permission/type'; +import { MongoEvaluation } from '../../../core/evaluation/task'; +import { EvaluationPermission } from '@fastgpt/global/support/permission/evaluation/controller'; +import type { DatasetFileSchema } from '@fastgpt/global/core/dataset/type'; import { getFileById } from '../../../common/file/gridfs/controller'; import { BucketNameEnum } from '@fastgpt/global/common/file/constants'; import { CommonErrEnum } from '@fastgpt/global/common/error/code/common'; import { Permission } from '@fastgpt/global/support/permission/controller'; -import type { DatasetFileSchema } from '@fastgpt/global/core/dataset/type'; -export const authEvalDatasetCollectionByTmbId = async ({ +// ================ 评估模块错误枚举 ================ +export const EvaluationAuthErrors = { + evaluationNotFound: 'Evaluation not found', + datasetNotFound: 'Evaluation dataset not found', + metricNotFound: 'Evaluation metric not found', + permissionDenied: 'Permission denied', + evaluationIdRequired: 'Evaluation ID is required', + datasetIdRequired: 'Evaluation dataset ID is required', + metricIdRequired: 'Evaluation metric ID is required' +} as const; + +// ================ 评估任务权限验证 ================ +export const authEvaluationByTmbId = async ({ tmbId, - collectionId, + evaluationId, per, - isRoot = false + isRoot }: { tmbId: string; - collectionId: string; + evaluationId: string; per: PermissionValueType; isRoot?: boolean; -}): Promise<{ - collection: EvalDatasetCollectionSchemaType; -}> => { - const [{ teamId, permission: tmbPer }, collection] = await Promise.all([ - getTmbInfoByTmbId({ tmbId }), - MongoEvalDatasetCollection.findOne({ _id: collectionId }).lean() - ]); - // TODO: error code - if (!collection) { - return Promise.reject('Evaluation dataset collection not found'); +}): Promise<{ evaluation: EvaluationDetailType }> => { + const { teamId, permission: tmbPer } = await getTmbInfoByTmbId({ tmbId }); + + const evaluation = await MongoEvaluation.findOne({ _id: evaluationId }).lean(); + if (!evaluation) { + return Promise.reject(EvaluationAuthErrors.evaluationNotFound); + } + + // Root用户权限特殊处理 + if (isRoot) { + return { + evaluation: { + ...evaluation, + permission: new EvaluationPermission({ isOwner: true }) + } + }; } - if (String(collection.teamId) !== teamId) { - return Promise.reject('Unauthorized access to evaluation dataset collection'); + // 团队权限验证 + if (String(evaluation.teamId) !== teamId) { + return Promise.reject(EvaluationAuthErrors.evaluationNotFound); } - // Check if user is owner or has permission - const isOwner = tmbPer.isOwner || String(collection.tmbId) === String(tmbId); + // 所有者检查 + const isOwner = tmbPer.isOwner || String(evaluation.tmbId) === String(tmbId); + + // 权限计算 + const { Per } = await (async () => { + if (isOwner) { + return { Per: new EvaluationPermission({ isOwner: true }) }; + } + + // 获取评估资源的权限 + const role = await getResourcePermission({ + teamId, + tmbId, + resourceId: evaluationId, + resourceType: PerResourceTypeEnum.evaluation + }); + + return { Per: new EvaluationPermission({ role, isOwner }) }; + })(); - if (!isRoot && !isOwner) { - return Promise.reject('Unauthorized access to evaluation dataset collection'); + // 权限验证 + if (!Per.checkPer(per)) { + return Promise.reject(EvaluationAuthErrors.permissionDenied); } - return { collection }; + return { + evaluation: { + ...evaluation, + permission: Per + } + }; }; -export const authEvalDatasetCollection = async ({ - collectionId, - per, +export const authEvaluation = async ({ + evaluationId, + per = ReadPermissionVal, ...props }: AuthModeType & { - collectionId: string; - per: PermissionValueType; -}): Promise<{ - userId: string; - teamId: string; - tmbId: string; - collection: EvalDatasetCollectionSchemaType; - isRoot: boolean; -}> => { - const result = await parseHeaderCert(props); + evaluationId: string; + per?: PermissionValueType; +}): Promise< + AuthResponseType & { + evaluation: EvaluationDetailType; + } +> => { + const result = await parseHeaderCert({ + ...props, + authApiKey: true // 添加API Key支持 + }); const { tmbId } = result; - if (!collectionId) { - return Promise.reject('Collection ID is required'); + if (!evaluationId) { + return Promise.reject(EvaluationAuthErrors.evaluationIdRequired); } - const { collection } = await authEvalDatasetCollectionByTmbId({ + const { evaluation } = await authEvaluationByTmbId({ tmbId, - collectionId, + evaluationId, per, isRoot: result.isRoot }); return { - userId: result.userId, - teamId: result.teamId, - tmbId: result.tmbId, - collection, + ...result, + permission: evaluation.permission, + evaluation + }; +}; + +// ================ 评估数据集权限验证 ================ +export const authEvalDatasetByTmbId = async ({ + tmbId, + datasetId, + per, + isRoot +}: { + tmbId: string; + datasetId: string; + per: PermissionValueType; + isRoot?: boolean; +}): Promise<{ dataset: any }> => { + const { MongoEvalDatasetCollection } = await import( + '../../../core/evaluation/dataset/evalDatasetCollectionSchema' + ); + const { teamId, permission: tmbPer } = await getTmbInfoByTmbId({ tmbId }); + + const dataset = await MongoEvalDatasetCollection.findOne({ _id: datasetId }).lean(); + if (!dataset) { + return Promise.reject(EvaluationAuthErrors.datasetNotFound); + } + + // Root用户权限特殊处理 + if (isRoot) { + return { + dataset: { + ...dataset, + permission: new EvaluationPermission({ isOwner: true }) + } + }; + } + + // 团队权限验证 + if (String(dataset.teamId) !== teamId) { + return Promise.reject(EvaluationAuthErrors.datasetNotFound); + } + + // 所有者检查 + const isOwner = tmbPer.isOwner || String(dataset.tmbId) === String(tmbId); + + // 权限计算 - 使用evaluation资源类型 + const { Per } = await (async () => { + if (isOwner) { + return { Per: new EvaluationPermission({ isOwner: true }) }; + } + + // 获取evaluation资源的权限(evalDataset复用evaluation权限) + const role = await getResourcePermission({ + teamId, + tmbId, + resourceId: datasetId, + resourceType: PerResourceTypeEnum.evaluation + }); + + return { Per: new EvaluationPermission({ role, isOwner }) }; + })(); + + // 权限验证 + if (!Per.checkPer(per)) { + return Promise.reject(EvaluationAuthErrors.permissionDenied); + } + + return { + dataset: { + ...dataset, + permission: Per + } + }; +}; + +export const authEvalDataset = async ({ + datasetId, + per = ReadPermissionVal, + ...props +}: AuthModeType & { + datasetId: string; + per?: PermissionValueType; +}): Promise< + AuthResponseType & { + dataset: any; + } +> => { + const result = await parseHeaderCert({ + ...props, + authApiKey: true // 添加API Key支持 + }); + const { tmbId } = result; + + if (!datasetId) { + return Promise.reject(EvaluationAuthErrors.datasetIdRequired); + } + + const { dataset } = await authEvalDatasetByTmbId({ + tmbId, + datasetId, + per, isRoot: result.isRoot + }); + + return { + ...result, + permission: dataset.permission, + dataset }; }; @@ -122,4 +277,108 @@ export const authEvalDatasetCollectionFile = async ({ permission, file }; -}; \ No newline at end of file +}; + +// ================ 评估指标权限验证 ================ +export const authEvalMetricByTmbId = async ({ + tmbId, + metricId, + per, + isRoot +}: { + tmbId: string; + metricId: string; + per: PermissionValueType; + isRoot?: boolean; +}): Promise<{ metric: any }> => { + const { MongoEvalMetric } = await import('../../../core/evaluation/metric/schema'); + const { teamId, permission: tmbPer } = await getTmbInfoByTmbId({ tmbId }); + + const metric = await MongoEvalMetric.findOne({ _id: metricId }).lean(); + if (!metric) { + return Promise.reject(EvaluationAuthErrors.metricNotFound); + } + + // Root用户权限特殊处理 + if (isRoot) { + return { + metric: { + ...metric, + permission: new EvaluationPermission({ isOwner: true }) + } + }; + } + + // 团队权限验证 + if (String(metric.teamId) !== teamId) { + return Promise.reject(EvaluationAuthErrors.metricNotFound); + } + + // 所有者检查 + const isOwner = tmbPer.isOwner || String(metric.tmbId) === String(tmbId); + + // 权限计算 - 使用evaluation资源类型 + const { Per } = await (async () => { + if (isOwner) { + return { Per: new EvaluationPermission({ isOwner: true }) }; + } + + // 获取evaluation资源的权限(evalMetric复用evaluation权限) + const role = await getResourcePermission({ + teamId, + tmbId, + resourceId: metricId, + resourceType: PerResourceTypeEnum.evaluation + }); + + return { Per: new EvaluationPermission({ role, isOwner }) }; + })(); + + // 权限验证 + if (!Per.checkPer(per)) { + return Promise.reject(EvaluationAuthErrors.permissionDenied); + } + + return { + metric: { + ...metric, + permission: Per + } + }; +}; + +export const authEvalMetric = async ({ + metricId, + per = ReadPermissionVal, + ...props +}: AuthModeType & { + metricId: string; + per?: PermissionValueType; +}): Promise< + AuthResponseType & { + metric: any; + } +> => { + const result = await parseHeaderCert({ + ...props, + authApiKey: true // 添加API Key支持 + }); + const { tmbId } = result; + + if (!metricId) { + return Promise.reject(EvaluationAuthErrors.metricIdRequired); + } + + const { metric } = await authEvalMetricByTmbId({ + tmbId, + metricId, + per, + isRoot: result.isRoot + }); + + return { + ...result, + permission: metric.permission, + metric + }; +}; diff --git a/packages/web/i18n/en/account_team.json b/packages/web/i18n/en/account_team.json index 645ab01ab624..4fe3b97e8ee3 100644 --- a/packages/web/i18n/en/account_team.json +++ b/packages/web/i18n/en/account_team.json @@ -230,6 +230,8 @@ "permission_appCreate_tip": "Create apps in the root directory. (Permissions within folders are controlled by the folder.)", "permission_datasetCreate": "Create knowledge base", "permission_datasetCreate_Tip": "Create knowledge bases in the root directory. (Permissions within folders are controlled by the folder.)", + "permission_evaluationCreate": "Create Evaluation", + "permission_evaluationCreate_Tip": "Can create evaluation tasks, evaluation metrics and evaluation datasets", "permission_manage": "Administrator", "permission_manage_tip": "Manage members, create groups, manage all groups, and assign permissions to groups and members.", "please_bind_contact": "Please specify contact information.", diff --git a/packages/web/i18n/zh-CN/account_team.json b/packages/web/i18n/zh-CN/account_team.json index e2879b1b0b5a..3549be5c845e 100644 --- a/packages/web/i18n/zh-CN/account_team.json +++ b/packages/web/i18n/zh-CN/account_team.json @@ -223,6 +223,8 @@ "permission_appCreate_tip": "可以在根目录创建应用,(文件夹下的创建权限由文件夹控制)", "permission_datasetCreate": "创建知识库", "permission_datasetCreate_Tip": "可以在根目录创建知识库,(文件夹下的创建权限由文件夹控制)", + "permission_evaluationCreate": "创建评估", + "permission_evaluationCreate_Tip": "可以创建评估任务、评估指标和评估数据集", "permission_manage": "管理员", "permission_manage_tip": "可以管理成员、创建群组、管理所有群组、为群组和成员分配权限", "please_bind_contact": "请绑定联系方式", diff --git a/projects/app/src/pageComponents/account/team/PermissionManage/index.tsx b/projects/app/src/pageComponents/account/team/PermissionManage/index.tsx index 3feef0e7d0dc..bc689a6e52dc 100644 --- a/projects/app/src/pageComponents/account/team/PermissionManage/index.tsx +++ b/projects/app/src/pageComponents/account/team/PermissionManage/index.tsx @@ -33,6 +33,8 @@ import { TeamAppCreateRoleVal, TeamDatasetCreatePermissionVal, TeamDatasetCreateRoleVal, + TeamEvaluationCreatePermissionVal, + TeamEvaluationCreateRoleVal, TeamManagePermissionVal, TeamManageRoleVal, TeamRoleList @@ -234,6 +236,12 @@ function PermissionManage({ + + + {t('account_team:permission_evaluationCreate')} + + + {t('account_team:permission_manage')} @@ -285,6 +293,12 @@ function PermissionManage({ clbPer={member.permission} id={member.tmbId!} /> + + + ) { } if (bucketName === 'evaluation') { const evalData = data as UploadEvaluationFileProps; - const authData = await authEvalDatasetCollection({ - collectionId: evalData.collectionId, + const authData = await authEvalDataset({ + datasetId: evalData.collectionId, per: WritePermissionVal, req, authToken: true, diff --git a/projects/app/src/pages/api/core/evaluation/dataset/collection/create.ts b/projects/app/src/pages/api/core/evaluation/dataset/collection/create.ts index 6ed20ad44e4b..624f824ae367 100644 --- a/projects/app/src/pages/api/core/evaluation/dataset/collection/create.ts +++ b/projects/app/src/pages/api/core/evaluation/dataset/collection/create.ts @@ -1,12 +1,11 @@ import type { ApiRequestProps } from '@fastgpt/service/type/next'; import { NextAPI } from '@/service/middleware/entry'; -import { WritePermissionVal } from '@fastgpt/global/support/permission/constant'; -import { authUserPer } from '@fastgpt/service/support/permission/user/auth'; import { mongoSessionRun } from '@fastgpt/service/common/mongo/sessionRun'; import { MongoEvalDatasetCollection } from '@fastgpt/service/core/evaluation/dataset/evalDatasetCollectionSchema'; import type { createEvalDatasetCollectionBody } from '@fastgpt/global/core/evaluation/dataset/api'; import { addAuditLog } from '@fastgpt/service/support/user/audit/util'; import { AuditEventEnum } from '@fastgpt/global/support/user/audit/constants'; +import { authEvaluationDatasetCreate } from '@fastgpt/service/core/evaluation/common'; export type EvalDatasetCollectionCreateQuery = {}; export type EvalDatasetCollectionCreateBody = createEvalDatasetCollectionBody; @@ -17,6 +16,13 @@ async function handler( ): Promise { const { name, description = '' } = req.body; + // Authentication and authorization + const { teamId, tmbId } = await authEvaluationDatasetCreate({ + req, + authApiKey: true, + authToken: true + }); + if (!name || typeof name !== 'string' || name.trim().length === 0) { return Promise.reject('Name is required and must be a non-empty string'); } @@ -33,14 +39,6 @@ async function handler( return Promise.reject('Description must be less than 100 characters'); } - // Authentication and authorization - const { teamId, tmbId } = await authUserPer({ - req, - authToken: true, - authApiKey: true, - per: WritePermissionVal - }); - // Check for name conflicts within team const existingDataset = await MongoEvalDatasetCollection.findOne({ teamId, diff --git a/projects/app/src/pages/api/core/evaluation/dataset/collection/delete.ts b/projects/app/src/pages/api/core/evaluation/dataset/collection/delete.ts index 8f2fea764f5a..005647101e88 100644 --- a/projects/app/src/pages/api/core/evaluation/dataset/collection/delete.ts +++ b/projects/app/src/pages/api/core/evaluation/dataset/collection/delete.ts @@ -1,7 +1,5 @@ import type { ApiRequestProps } from '@fastgpt/service/type/next'; import { NextAPI } from '@/service/middleware/entry'; -import { WritePermissionVal } from '@fastgpt/global/support/permission/constant'; -import { authUserPer } from '@fastgpt/service/support/permission/user/auth'; import { mongoSessionRun } from '@fastgpt/service/common/mongo/sessionRun'; import { MongoEvalDatasetCollection } from '@fastgpt/service/core/evaluation/dataset/evalDatasetCollectionSchema'; import { MongoEvalDatasetData } from '@fastgpt/service/core/evaluation/dataset/evalDatasetDataSchema'; @@ -12,6 +10,7 @@ import { removeEvalDatasetDataSynthesizeJobsRobust } from '@fastgpt/service/core import { addLog } from '@fastgpt/service/common/system/log'; import { addAuditLog } from '@fastgpt/service/support/user/audit/util'; import { AuditEventEnum } from '@fastgpt/global/support/user/audit/constants'; +import { authEvaluationDatasetWrite } from '@fastgpt/service/core/evaluation/common'; export type EvalDatasetCollectionDeleteQuery = deleteEvalDatasetCollectionQuery; export type EvalDatasetCollectionDeleteBody = {}; @@ -26,11 +25,10 @@ async function handler( return Promise.reject('collectionId is required and must be a string'); } - const { teamId, tmbId } = await authUserPer({ + const { teamId, tmbId } = await authEvaluationDatasetWrite(collectionId, { req, - authToken: true, authApiKey: true, - per: WritePermissionVal + authToken: true }); let collectionName = ''; diff --git a/projects/app/src/pages/api/core/evaluation/dataset/collection/deleteTask.ts b/projects/app/src/pages/api/core/evaluation/dataset/collection/deleteTask.ts index 400f70760b72..7dd488b73a81 100644 --- a/projects/app/src/pages/api/core/evaluation/dataset/collection/deleteTask.ts +++ b/projects/app/src/pages/api/core/evaluation/dataset/collection/deleteTask.ts @@ -1,7 +1,5 @@ import type { ApiRequestProps } from '@fastgpt/service/type/next'; import { NextAPI } from '@/service/middleware/entry'; -import { authUserPer } from '@fastgpt/service/support/permission/user/auth'; -import { WritePermissionVal } from '@fastgpt/global/support/permission/constant'; import { MongoEvalDatasetCollection } from '@fastgpt/service/core/evaluation/dataset/evalDatasetCollectionSchema'; import { Types } from '@fastgpt/service/common/mongo'; import type { deleteTaskBody } from '@fastgpt/global/core/evaluation/dataset/api'; @@ -9,19 +7,19 @@ import { evalDatasetDataSynthesizeQueue } from '@fastgpt/service/core/evaluation import { addLog } from '@fastgpt/service/common/system/log'; import { addAuditLog } from '@fastgpt/service/support/user/audit/util'; import { AuditEventEnum } from '@fastgpt/global/support/user/audit/constants'; +import { authEvaluationDatasetWrite } from '@fastgpt/service/core/evaluation/common'; async function handler( req: ApiRequestProps ): Promise<{ success: boolean; message: string }> { - const { teamId, tmbId } = await authUserPer({ + const { collectionId, jobId } = req.body; + + const { teamId, tmbId } = await authEvaluationDatasetWrite(collectionId, { req, - authToken: true, authApiKey: true, - per: WritePermissionVal + authToken: true }); - const { collectionId, jobId } = req.body; - const collection = await MongoEvalDatasetCollection.findOne({ _id: new Types.ObjectId(collectionId), teamId: new Types.ObjectId(teamId) diff --git a/projects/app/src/pages/api/core/evaluation/dataset/collection/failedTasks.ts b/projects/app/src/pages/api/core/evaluation/dataset/collection/failedTasks.ts index 6a44753b3c26..82674f2a21a0 100644 --- a/projects/app/src/pages/api/core/evaluation/dataset/collection/failedTasks.ts +++ b/projects/app/src/pages/api/core/evaluation/dataset/collection/failedTasks.ts @@ -1,7 +1,5 @@ import type { ApiRequestProps } from '@fastgpt/service/type/next'; import { NextAPI } from '@/service/middleware/entry'; -import { authUserPer } from '@fastgpt/service/support/permission/user/auth'; -import { ReadPermissionVal } from '@fastgpt/global/support/permission/constant'; import { MongoEvalDatasetCollection } from '@fastgpt/service/core/evaluation/dataset/evalDatasetCollectionSchema'; import { Types } from '@fastgpt/service/common/mongo'; import type { @@ -9,19 +7,19 @@ import type { listFailedTasksResponse } from '@fastgpt/global/core/evaluation/dataset/api'; import { evalDatasetDataSynthesizeQueue } from '@fastgpt/service/core/evaluation/dataset/dataSynthesizeMq'; +import { authEvaluationDatasetRead } from '@fastgpt/service/core/evaluation/common'; async function handler( req: ApiRequestProps ): Promise { - const { teamId } = await authUserPer({ + const { collectionId } = req.body; + + const { teamId } = await authEvaluationDatasetRead(collectionId, { req, - authToken: true, authApiKey: true, - per: ReadPermissionVal + authToken: true }); - const { collectionId } = req.body; - const collection = await MongoEvalDatasetCollection.findOne({ _id: new Types.ObjectId(collectionId), teamId: new Types.ObjectId(teamId) diff --git a/projects/app/src/pages/api/core/evaluation/dataset/collection/list.ts b/projects/app/src/pages/api/core/evaluation/dataset/collection/list.ts index 05f3b1c52154..0bcbdc66af26 100644 --- a/projects/app/src/pages/api/core/evaluation/dataset/collection/list.ts +++ b/projects/app/src/pages/api/core/evaluation/dataset/collection/list.ts @@ -1,18 +1,20 @@ import type { ApiRequestProps } from '@fastgpt/service/type/next'; import { NextAPI } from '@/service/middleware/entry'; -import { authUserPer } from '@fastgpt/service/support/permission/user/auth'; -import { ReadPermissionVal } from '@fastgpt/global/support/permission/constant'; import { MongoEvalDatasetCollection } from '@fastgpt/service/core/evaluation/dataset/evalDatasetCollectionSchema'; -import { parsePaginationRequest } from '@fastgpt/service/common/api/pagination'; -import { Types } from '@fastgpt/service/common/mongo'; +import { EvaluationPermission } from '@fastgpt/global/support/permission/evaluation/controller'; +import { replaceRegChars } from '@fastgpt/global/common/string/tools'; +import { addSourceMember } from '@fastgpt/service/support/user/utils'; +import { sumPer } from '@fastgpt/global/support/permission/utils'; +import { Types } from 'mongoose'; import type { listEvalDatasetCollectionBody, listEvalDatasetCollectionResponse } from '@fastgpt/global/core/evaluation/dataset/api'; import type { EvalDatasetCollectionStatus } from '@fastgpt/global/core/evaluation/dataset/type'; import { EvalDatasetCollectionStatusEnum } from '@fastgpt/global/core/evaluation/dataset/constants'; -import { replaceRegChars } from '@fastgpt/global/common/string/tools'; import { evalDatasetDataSynthesizeQueue } from '@fastgpt/service/core/evaluation/dataset/dataSynthesizeMq'; +import { parsePaginationRequest } from '@fastgpt/service/common/api/pagination'; +import { getEvaluationPermissionAggregation } from '@fastgpt/service/core/evaluation/common'; async function getCollectionStatus(collectionId: string): Promise { try { @@ -47,95 +49,182 @@ async function getCollectionStatus(collectionId: string): Promise ): Promise { - const { teamId, tmbId } = await authUserPer({ - req, - authToken: true, - authApiKey: true, - per: ReadPermissionVal - }); - const { offset, pageSize } = parsePaginationRequest(req); const { searchKey } = req.body; - const match: Record = { + // API layer permission validation: get permission aggregation info + const { teamId, tmbId, isOwner, roleList, myGroupMap, myOrgSet } = + await getEvaluationPermissionAggregation({ + req, + authApiKey: true, + authToken: true + }); + + // Calculate resource IDs accessible by user + const myRoles = roleList.filter( + (item) => + String(item.tmbId) === String(tmbId) || + myGroupMap.has(String(item.groupId)) || + myOrgSet.has(String(item.orgId)) + ); + const accessibleIds = myRoles.map((item) => String(item.resourceId)); + + // Build unified filter conditions + const baseFilter: Record = { teamId: new Types.ObjectId(teamId) }; - // Add search filter if provided if (searchKey && typeof searchKey === 'string' && searchKey.trim().length > 0) { - match.name = { $regex: new RegExp(`${replaceRegChars(searchKey.trim())}`, 'i') }; + baseFilter.name = { $regex: new RegExp(`${replaceRegChars(searchKey.trim())}`, 'i') }; } - try { - // Execute aggregation with pagination - const [collections, total] = await Promise.all([ - MongoEvalDatasetCollection.aggregate(buildPipeline(match, offset, pageSize)), - MongoEvalDatasetCollection.countDocuments(match) - ]); - - // TODO: Audit Log - Log successful response - - const collectionsWithStatus = await Promise.all( - collections.map(async (item) => { - const status = await getCollectionStatus(String(item._id)); - return { - _id: String(item._id), - name: item.name, - description: item.description || '', - createTime: item.createTime, - updateTime: item.updateTime, - creatorAvatar: item.teamMember?.avatar, - creatorName: item.teamMember?.name, - status - }; - }) - ); - - return { - total, - list: collectionsWithStatus - }; - } catch (error) { - console.error('Database error in eval dataset collection list:', error); - throw error; + // Unified permission filtering logic + let finalFilter = baseFilter; + if (!isOwner) { + if (accessibleIds.length > 0) { + finalFilter = { + ...baseFilter, + $or: [ + { _id: { $in: accessibleIds.map((id) => new Types.ObjectId(id)) } }, + { tmbId: new Types.ObjectId(tmbId) } // Own datasets + ] + }; + } else { + // If no permission roles, can only access self-created datasets + finalFilter = { + ...baseFilter, + tmbId: new Types.ObjectId(tmbId) + }; + } } -} -const buildPipeline = (match: Record, offset: number, pageSize: number) => [ - { $match: match }, - { $sort: { createTime: -1 as const } }, - { $skip: offset }, - { $limit: pageSize }, - { - $lookup: { - from: 'team_members', - localField: 'tmbId', - foreignField: '_id', - as: 'teamMember' - } - }, - { - $addFields: { - teamMember: { $arrayElemAt: ['$teamMember', 0] } - } - }, - { - $project: { - _id: 1, - name: 1, - description: 1, - createTime: 1, - updateTime: 1, - teamMember: { - avatar: 1, - name: 1 + const [collections, total] = await Promise.all([ + MongoEvalDatasetCollection.aggregate([ + { $match: finalFilter }, + { $sort: { createTime: -1 as const } }, + { $skip: offset }, + { $limit: pageSize }, + { + $lookup: { + from: 'team_members', + localField: 'tmbId', + foreignField: '_id', + as: 'teamMember' + } + }, + { + $addFields: { + teamMember: { $arrayElemAt: ['$teamMember', 0] } + } + }, + { + $project: { + _id: 1, + name: 1, + description: 1, + tmbId: 1, + createTime: 1, + updateTime: 1, + creatorAvatar: '$teamMember.avatar', + creatorName: '$teamMember.name' + } } - } - } -]; + ]), + MongoEvalDatasetCollection.countDocuments(finalFilter) + ]); + + const formatCollections = collections + .map((collection) => { + const getPer = (collectionId: string) => { + const tmbRole = myRoles.find( + (item) => String(item.resourceId) === collectionId && !!item.tmbId + )?.permission; + const groupRole = sumPer( + ...myRoles + .filter( + (item) => String(item.resourceId) === collectionId && (!!item.groupId || !!item.orgId) + ) + .map((item) => item.permission) + ); + const permission = new EvaluationPermission({ + role: tmbRole ?? groupRole, + isOwner: String(collection.tmbId) === String(tmbId) || isOwner + }); + + return permission; + }; + + const getClbCount = (collectionId: string) => { + return roleList.filter((item) => String(item.resourceId) === String(collectionId)).length; + }; + + const getPrivateStatus = (collectionId: string) => { + const collaboratorCount = getClbCount(collectionId); + if (isOwner) { + return collaboratorCount <= 1; + } + return ( + collaboratorCount === 0 || + (collaboratorCount === 1 && String(collection.tmbId) === String(tmbId)) + ); + }; + + return { + _id: String(collection._id), + name: collection.name, + description: collection.description || '', + tmbId: collection.tmbId, + createTime: collection.createTime, + updateTime: collection.updateTime, + creatorAvatar: collection.creatorAvatar, + creatorName: collection.creatorName, + permission: getPer(String(collection._id)), + private: getPrivateStatus(String(collection._id)) + }; + }) + .filter((collection) => { + // Owner should have access to all collections + if (isOwner) { + return true; + } + return collection.permission.hasReadPer; + }); + + // Add status and source member info + const collectionsWithStatus = await Promise.all( + formatCollections.map(async (collection) => { + const status = await getCollectionStatus(String(collection._id)); + return { + ...collection, + status + }; + }) + ); + + const collectionsWithMember = await addSourceMember({ + list: collectionsWithStatus + }); + + // Remove tmbId from final response as it's not needed in the API response + const finalCollections = collectionsWithMember.map(({ tmbId, ...rest }) => rest); + + return { + list: finalCollections, + total: total + }; +} export default NextAPI(handler); diff --git a/projects/app/src/pages/api/core/evaluation/dataset/collection/qualityAssessmentBatch.ts b/projects/app/src/pages/api/core/evaluation/dataset/collection/qualityAssessmentBatch.ts index e9f7316192f4..844c33759089 100644 --- a/projects/app/src/pages/api/core/evaluation/dataset/collection/qualityAssessmentBatch.ts +++ b/projects/app/src/pages/api/core/evaluation/dataset/collection/qualityAssessmentBatch.ts @@ -1,7 +1,5 @@ import type { ApiRequestProps } from '@fastgpt/service/type/next'; import { NextAPI } from '@/service/middleware/entry'; -import { WritePermissionVal } from '@fastgpt/global/support/permission/constant'; -import { authUserPer } from '@fastgpt/service/support/permission/user/auth'; import { MongoEvalDatasetData } from '@fastgpt/service/core/evaluation/dataset/evalDatasetDataSchema'; import { MongoEvalDatasetCollection } from '@fastgpt/service/core/evaluation/dataset/evalDatasetCollectionSchema'; import { @@ -17,6 +15,7 @@ import { addLog } from '@fastgpt/service/common/system/log'; import { EvalDatasetDataQualityStatusEnum } from '@fastgpt/global/core/evaluation/dataset/constants'; import { addAuditLog } from '@fastgpt/service/support/user/audit/util'; import { AuditEventEnum } from '@fastgpt/global/support/user/audit/constants'; +import { authEvaluationDatasetWrite } from '@fastgpt/service/core/evaluation/common'; export type QualityAssessmentBatchQuery = {}; export type QualityAssessmentBatchBody = qualityAssessmentBatchBody; @@ -26,6 +25,12 @@ async function handler( ): Promise { const { collectionId, evalModel } = req.body; + const { teamId, tmbId } = await authEvaluationDatasetWrite(collectionId, { + req, + authApiKey: true, + authToken: true + }); + if (!collectionId || typeof collectionId !== 'string') { return { success: false, @@ -46,13 +51,6 @@ async function handler( }; } - const { teamId, tmbId } = await authUserPer({ - req, - authToken: true, - authApiKey: true, - per: WritePermissionVal - }); - const collection = await MongoEvalDatasetCollection.findOne({ _id: collectionId, teamId diff --git a/projects/app/src/pages/api/core/evaluation/dataset/collection/retryTask.ts b/projects/app/src/pages/api/core/evaluation/dataset/collection/retryTask.ts index 10c1e438bd88..5b03cd7561b3 100644 --- a/projects/app/src/pages/api/core/evaluation/dataset/collection/retryTask.ts +++ b/projects/app/src/pages/api/core/evaluation/dataset/collection/retryTask.ts @@ -1,7 +1,5 @@ import type { ApiRequestProps } from '@fastgpt/service/type/next'; import { NextAPI } from '@/service/middleware/entry'; -import { authUserPer } from '@fastgpt/service/support/permission/user/auth'; -import { WritePermissionVal } from '@fastgpt/global/support/permission/constant'; import { MongoEvalDatasetCollection } from '@fastgpt/service/core/evaluation/dataset/evalDatasetCollectionSchema'; import { Types } from '@fastgpt/service/common/mongo'; import type { retryTaskBody } from '@fastgpt/global/core/evaluation/dataset/api'; @@ -9,19 +7,19 @@ import { evalDatasetDataSynthesizeQueue } from '@fastgpt/service/core/evaluation import { addLog } from '@fastgpt/service/common/system/log'; import { addAuditLog } from '@fastgpt/service/support/user/audit/util'; import { AuditEventEnum } from '@fastgpt/global/support/user/audit/constants'; +import { authEvaluationDatasetWrite } from '@fastgpt/service/core/evaluation/common'; async function handler( req: ApiRequestProps ): Promise<{ success: boolean; message: string }> { - const { teamId, tmbId } = await authUserPer({ + const { collectionId, jobId } = req.body; + + const { teamId, tmbId } = await authEvaluationDatasetWrite(collectionId, { req, - authToken: true, authApiKey: true, - per: WritePermissionVal + authToken: true }); - const { collectionId, jobId } = req.body; - const collection = await MongoEvalDatasetCollection.findOne({ _id: new Types.ObjectId(collectionId), teamId: new Types.ObjectId(teamId) diff --git a/projects/app/src/pages/api/core/evaluation/dataset/collection/update.ts b/projects/app/src/pages/api/core/evaluation/dataset/collection/update.ts index bac8f654a0df..30538e0fb4c3 100644 --- a/projects/app/src/pages/api/core/evaluation/dataset/collection/update.ts +++ b/projects/app/src/pages/api/core/evaluation/dataset/collection/update.ts @@ -1,12 +1,11 @@ import type { ApiRequestProps } from '@fastgpt/service/type/next'; import { NextAPI } from '@/service/middleware/entry'; -import { WritePermissionVal } from '@fastgpt/global/support/permission/constant'; -import { authUserPer } from '@fastgpt/service/support/permission/user/auth'; import { mongoSessionRun } from '@fastgpt/service/common/mongo/sessionRun'; import { MongoEvalDatasetCollection } from '@fastgpt/service/core/evaluation/dataset/evalDatasetCollectionSchema'; import type { updateEvalDatasetCollectionBody } from '@fastgpt/global/core/evaluation/dataset/api'; import { addAuditLog } from '@fastgpt/service/support/user/audit/util'; import { AuditEventEnum } from '@fastgpt/global/support/user/audit/constants'; +import { authEvaluationDatasetWrite } from '@fastgpt/service/core/evaluation/common'; export type EvalDatasetCollectionUpdateQuery = {}; export type EvalDatasetCollectionUpdateBody = updateEvalDatasetCollectionBody; @@ -16,6 +15,12 @@ async function handler( ): Promise { const { collectionId, name, description = '' } = req.body; + const { teamId, tmbId } = await authEvaluationDatasetWrite(collectionId, { + req, + authApiKey: true, + authToken: true + }); + if (!collectionId || typeof collectionId !== 'string' || collectionId.trim().length === 0) { return Promise.reject('Collection ID is required and must be a non-empty string'); } @@ -36,15 +41,6 @@ async function handler( return Promise.reject('Description must be less than 100 characters'); } - // TODO: Authentication check - verify user is authenticated via cookie or token - // TODO: Authorization check - verify user has write permissions for this resource - const { teamId, tmbId } = await authUserPer({ - req, - authToken: true, - authApiKey: true, - per: WritePermissionVal - }); - // Check if collection exists and belongs to the team const existingCollection = await MongoEvalDatasetCollection.findOne({ _id: collectionId, diff --git a/projects/app/src/pages/api/core/evaluation/dataset/data/create.ts b/projects/app/src/pages/api/core/evaluation/dataset/data/create.ts index 99efe9156b2c..affee903c3a8 100644 --- a/projects/app/src/pages/api/core/evaluation/dataset/data/create.ts +++ b/projects/app/src/pages/api/core/evaluation/dataset/data/create.ts @@ -1,7 +1,5 @@ import type { ApiRequestProps } from '@fastgpt/service/type/next'; import { NextAPI } from '@/service/middleware/entry'; -import { WritePermissionVal } from '@fastgpt/global/support/permission/constant'; -import { authUserPer } from '@fastgpt/service/support/permission/user/auth'; import { mongoSessionRun } from '@fastgpt/service/common/mongo/sessionRun'; import { MongoEvalDatasetData } from '@fastgpt/service/core/evaluation/dataset/evalDatasetDataSchema'; import { MongoEvalDatasetCollection } from '@fastgpt/service/core/evaluation/dataset/evalDatasetCollectionSchema'; @@ -12,6 +10,7 @@ import { import type { createEvalDatasetDataBody } from '@fastgpt/global/core/evaluation/dataset/api'; import { addAuditLog } from '@fastgpt/service/support/user/audit/util'; import { AuditEventEnum } from '@fastgpt/global/support/user/audit/constants'; +import { authEvaluationDatasetDataCreate } from '@fastgpt/service/core/evaluation/common'; export type EvalDatasetDataCreateQuery = {}; export type EvalDatasetDataCreateBody = createEvalDatasetDataBody; @@ -23,6 +22,12 @@ async function handler( const { collectionId, userInput, actualOutput, expectedOutput, context, retrievalContext } = req.body; + const { teamId, tmbId } = await authEvaluationDatasetDataCreate(collectionId, { + req, + authToken: true, + authApiKey: true + }); + if (!collectionId || typeof collectionId !== 'string') { return Promise.reject('collectionId is required and must be a string'); } @@ -54,13 +59,6 @@ async function handler( return Promise.reject('retrievalContext must be an array of strings if provided'); } - const { teamId, tmbId } = await authUserPer({ - req, - authToken: true, - authApiKey: true, - per: WritePermissionVal - }); - // Verify collection exists and belongs to the team const collection = await MongoEvalDatasetCollection.findOne({ _id: collectionId, diff --git a/projects/app/src/pages/api/core/evaluation/dataset/data/delete.ts b/projects/app/src/pages/api/core/evaluation/dataset/data/delete.ts index 28b823011822..c40493aec8e0 100644 --- a/projects/app/src/pages/api/core/evaluation/dataset/data/delete.ts +++ b/projects/app/src/pages/api/core/evaluation/dataset/data/delete.ts @@ -1,7 +1,5 @@ import type { ApiRequestProps } from '@fastgpt/service/type/next'; import { NextAPI } from '@/service/middleware/entry'; -import { WritePermissionVal } from '@fastgpt/global/support/permission/constant'; -import { authUserPer } from '@fastgpt/service/support/permission/user/auth'; import { mongoSessionRun } from '@fastgpt/service/common/mongo/sessionRun'; import { MongoEvalDatasetData } from '@fastgpt/service/core/evaluation/dataset/evalDatasetDataSchema'; import { MongoEvalDatasetCollection } from '@fastgpt/service/core/evaluation/dataset/evalDatasetCollectionSchema'; @@ -13,6 +11,7 @@ import { import { addLog } from '@fastgpt/service/common/system/log'; import { addAuditLog } from '@fastgpt/service/support/user/audit/util'; import { AuditEventEnum } from '@fastgpt/global/support/user/audit/constants'; +import { authEvaluationDatasetDataUpdateById } from '@fastgpt/service/core/evaluation/common'; export type EvalDatasetDataDeleteQuery = deleteEvalDatasetDataQuery; export type EvalDatasetDataDeleteBody = {}; @@ -23,17 +22,16 @@ async function handler( ): Promise { const { dataId } = req.query; - if (!dataId || typeof dataId !== 'string' || dataId.trim().length === 0) { - return Promise.reject('dataId is required and must be a string'); - } - - const { teamId, tmbId } = await authUserPer({ + const { teamId, tmbId, collectionId } = await authEvaluationDatasetDataUpdateById(dataId, { req, authToken: true, - authApiKey: true, - per: WritePermissionVal + authApiKey: true }); + if (!dataId || typeof dataId !== 'string' || dataId.trim().length === 0) { + return Promise.reject('dataId is required and must be a string'); + } + let collectionName = ''; await mongoSessionRun(async (session) => { @@ -82,7 +80,7 @@ async function handler( addLog.info('Evaluation dataset data deleted successfully', { dataId, - datasetId: existingData.datasetId, + datasetId: collectionId, teamId }); }); diff --git a/projects/app/src/pages/api/core/evaluation/dataset/data/fileId.ts b/projects/app/src/pages/api/core/evaluation/dataset/data/fileId.ts index 0ac5ec43333b..adbc702c4ccc 100644 --- a/projects/app/src/pages/api/core/evaluation/dataset/data/fileId.ts +++ b/projects/app/src/pages/api/core/evaluation/dataset/data/fileId.ts @@ -15,6 +15,7 @@ import { addEvalDatasetDataQualityJob } from '@fastgpt/service/core/evaluation/d import { authEvalDatasetCollectionFile } from '@fastgpt/service/support/permission/evaluation/auth'; import { addAuditLog } from '@fastgpt/service/support/user/audit/util'; import { AuditEventEnum } from '@fastgpt/global/support/user/audit/constants'; +import { authEvaluationDatasetDataWrite } from '@fastgpt/service/core/evaluation/common'; export type EvalDatasetImportFromFileQuery = {}; export type EvalDatasetImportFromFileBody = importEvalDatasetFromFileBody; @@ -151,6 +152,12 @@ async function handler( ): Promise { const { fileId, collectionId, enableQualityEvaluation, qualityEvaluationModel } = req.body; + const { teamId, tmbId } = await authEvaluationDatasetDataWrite(collectionId, { + req, + authToken: true, + authApiKey: true + }); + if (!fileId || typeof fileId !== 'string') { return 'fileId is required and must be a string'; } @@ -170,7 +177,8 @@ async function handler( return 'qualityEvaluationModel is required when enableQualityEvaluation is true'; } - const { file, teamId, tmbId } = await authEvalDatasetCollectionFile({ + // 先进行文件认证 + const { file } = await authEvalDatasetCollectionFile({ req, authToken: true, authApiKey: true, @@ -192,7 +200,6 @@ async function handler( if (String(datasetCollection.teamId) !== teamId) { return 'No permission to access this dataset collection'; } - try { // Read and parse CSV file const { rawText } = await readFileContentFromMongo({ diff --git a/projects/app/src/pages/api/core/evaluation/dataset/data/list.ts b/projects/app/src/pages/api/core/evaluation/dataset/data/list.ts index 14ddd8e6b347..5af4fdf107b4 100644 --- a/projects/app/src/pages/api/core/evaluation/dataset/data/list.ts +++ b/projects/app/src/pages/api/core/evaluation/dataset/data/list.ts @@ -1,9 +1,6 @@ import type { ApiRequestProps } from '@fastgpt/service/type/next'; import { NextAPI } from '@/service/middleware/entry'; -import { authUserPer } from '@fastgpt/service/support/permission/user/auth'; -import { ReadPermissionVal } from '@fastgpt/global/support/permission/constant'; import { MongoEvalDatasetData } from '@fastgpt/service/core/evaluation/dataset/evalDatasetDataSchema'; -import { MongoEvalDatasetCollection } from '@fastgpt/service/core/evaluation/dataset/evalDatasetCollectionSchema'; import { parsePaginationRequest } from '@fastgpt/service/common/api/pagination'; import { Types } from '@fastgpt/service/common/mongo'; import type { @@ -12,17 +9,11 @@ import type { } from '@fastgpt/global/core/evaluation/dataset/api'; import { replaceRegChars } from '@fastgpt/global/common/string/tools'; import { EvalDatasetDataKeyEnum } from '@fastgpt/global/core/evaluation/dataset/constants'; +import { authEvaluationDatasetDataRead } from '@fastgpt/service/core/evaluation/common'; async function handler( req: ApiRequestProps ): Promise { - const { teamId, tmbId } = await authUserPer({ - req, - authToken: true, - authApiKey: true, - per: ReadPermissionVal - }); - // Parse request parameters const { offset, pageSize } = parsePaginationRequest(req); const { collectionId, searchKey } = req.body; @@ -32,18 +23,14 @@ async function handler( throw new Error('Collection ID is required'); } - // TODO: Audit Log - Log request attempt with parameters - console.log(`[AUDIT] User requested eval dataset data list for collection: ${collectionId}`); - - // Verify collection exists and belongs to team - const collection = await MongoEvalDatasetCollection.findOne({ - _id: new Types.ObjectId(collectionId), - teamId: new Types.ObjectId(teamId) + await authEvaluationDatasetDataRead(collectionId, { + req, + authToken: true, + authApiKey: true }); - if (!collection) { - throw new Error('Collection not found or access denied'); - } + // TODO: Audit Log - Log request attempt with parameters + console.log(`[AUDIT] User requested eval dataset data list for collection: ${collectionId}`); // Build MongoDB match criteria const match: Record = { diff --git a/projects/app/src/pages/api/core/evaluation/dataset/data/qualityAssessment.ts b/projects/app/src/pages/api/core/evaluation/dataset/data/qualityAssessment.ts index 0b1945d492bb..707813b074c7 100644 --- a/projects/app/src/pages/api/core/evaluation/dataset/data/qualityAssessment.ts +++ b/projects/app/src/pages/api/core/evaluation/dataset/data/qualityAssessment.ts @@ -1,7 +1,5 @@ import type { ApiRequestProps } from '@fastgpt/service/type/next'; import { NextAPI } from '@/service/middleware/entry'; -import { WritePermissionVal } from '@fastgpt/global/support/permission/constant'; -import { authUserPer } from '@fastgpt/service/support/permission/user/auth'; import { MongoEvalDatasetData } from '@fastgpt/service/core/evaluation/dataset/evalDatasetDataSchema'; import { MongoEvalDatasetCollection } from '@fastgpt/service/core/evaluation/dataset/evalDatasetCollectionSchema'; import { @@ -13,6 +11,7 @@ import type { qualityAssessmentBody } from '@fastgpt/global/core/evaluation/data import { EvalDatasetDataQualityStatusEnum } from '@fastgpt/global/core/evaluation/dataset/constants'; import { addAuditLog } from '@fastgpt/service/support/user/audit/util'; import { AuditEventEnum } from '@fastgpt/global/support/user/audit/constants'; +import { authEvaluationDatasetDataUpdateById } from '@fastgpt/service/core/evaluation/common'; export type QualityAssessmentQuery = {}; export type QualityAssessmentBody = qualityAssessmentBody; @@ -23,6 +22,12 @@ async function handler( ): Promise { const { dataId, evalModel } = req.body; + const { teamId, tmbId } = await authEvaluationDatasetDataUpdateById(dataId, { + req, + authToken: true, + authApiKey: true + }); + if (!dataId || typeof dataId !== 'string') { return 'dataId is required and must be a string'; } @@ -31,13 +36,6 @@ async function handler( return 'evalModel is required and must be a string'; } - const { teamId, tmbId } = await authUserPer({ - req, - authToken: true, - authApiKey: true, - per: WritePermissionVal - }); - const datasetData = await MongoEvalDatasetData.findById(dataId); if (!datasetData) { return 'Dataset data not found'; diff --git a/projects/app/src/pages/api/core/evaluation/dataset/data/smartGenerate.ts b/projects/app/src/pages/api/core/evaluation/dataset/data/smartGenerate.ts index 93c05e82d4be..5bd9f4dade21 100644 --- a/projects/app/src/pages/api/core/evaluation/dataset/data/smartGenerate.ts +++ b/projects/app/src/pages/api/core/evaluation/dataset/data/smartGenerate.ts @@ -1,7 +1,5 @@ import type { ApiRequestProps } from '@fastgpt/service/type/next'; import { NextAPI } from '@/service/middleware/entry'; -import { WritePermissionVal } from '@fastgpt/global/support/permission/constant'; -import { authUserPer } from '@fastgpt/service/support/permission/user/auth'; import { MongoEvalDatasetCollection } from '@fastgpt/service/core/evaluation/dataset/evalDatasetCollectionSchema'; import { MongoDatasetCollection } from '@fastgpt/service/core/dataset/collection/schema'; import { MongoDatasetData } from '@fastgpt/service/core/dataset/data/schema'; @@ -9,6 +7,7 @@ import type { smartGenerateEvalDatasetBody } from '@fastgpt/global/core/evaluati import { addEvalDatasetSmartGenerateJob } from '@fastgpt/service/core/evaluation/dataset/smartGenerateMq'; import { addAuditLog } from '@fastgpt/service/support/user/audit/util'; import { AuditEventEnum } from '@fastgpt/global/support/user/audit/constants'; +import { authEvaluationDatasetGenFromKnowledgeBase } from '@fastgpt/service/core/evaluation/common'; export type SmartGenerateEvalDatasetQuery = {}; export type SmartGenerateEvalDatasetBody = smartGenerateEvalDatasetBody; @@ -19,6 +18,16 @@ async function handler( ): Promise { const { collectionId, datasetCollectionIds, count, intelligentGenerationModel } = req.body; + const { teamId, tmbId } = await authEvaluationDatasetGenFromKnowledgeBase( + collectionId, + datasetCollectionIds, + { + req, + authToken: true, + authApiKey: true + } + ); + // Parameter validation if (!collectionId || typeof collectionId !== 'string') { return Promise.reject('collectionId is required and must be a string'); @@ -36,13 +45,6 @@ async function handler( return Promise.reject('intelligentGenerationModel is required and must be a string'); } - const { teamId, tmbId } = await authUserPer({ - req, - authToken: true, - authApiKey: true, - per: WritePermissionVal - }); - const evalDatasetCollection = await MongoEvalDatasetCollection.findById(collectionId); if (!evalDatasetCollection) { return Promise.reject('Evaluation dataset collection not found'); diff --git a/projects/app/src/pages/api/core/evaluation/dataset/data/update.ts b/projects/app/src/pages/api/core/evaluation/dataset/data/update.ts index c367961ce894..d9767d0fa17a 100644 --- a/projects/app/src/pages/api/core/evaluation/dataset/data/update.ts +++ b/projects/app/src/pages/api/core/evaluation/dataset/data/update.ts @@ -1,7 +1,5 @@ import type { ApiRequestProps } from '@fastgpt/service/type/next'; import { NextAPI } from '@/service/middleware/entry'; -import { WritePermissionVal } from '@fastgpt/global/support/permission/constant'; -import { authUserPer } from '@fastgpt/service/support/permission/user/auth'; import { mongoSessionRun } from '@fastgpt/service/common/mongo/sessionRun'; import { MongoEvalDatasetData } from '@fastgpt/service/core/evaluation/dataset/evalDatasetDataSchema'; import { MongoEvalDatasetCollection } from '@fastgpt/service/core/evaluation/dataset/evalDatasetCollectionSchema'; @@ -14,6 +12,7 @@ import { addLog } from '@fastgpt/service/common/system/log'; import { EvalDatasetDataKeyEnum } from '@fastgpt/global/core/evaluation/dataset/constants'; import { addAuditLog } from '@fastgpt/service/support/user/audit/util'; import { AuditEventEnum } from '@fastgpt/global/support/user/audit/constants'; +import { authEvaluationDatasetDataUpdateById } from '@fastgpt/service/core/evaluation/common'; export type EvalDatasetDataUpdateQuery = {}; export type EvalDatasetDataUpdateBody = updateEvalDatasetDataBody; @@ -33,6 +32,12 @@ async function handler( qualityEvaluationModel } = req.body; + const { teamId, tmbId } = await authEvaluationDatasetDataUpdateById(dataId, { + req, + authToken: true, + authApiKey: true + }); + if (!dataId || typeof dataId !== 'string') { return Promise.reject('dataId is required and must be a string'); } @@ -77,13 +82,6 @@ async function handler( ); } - const { teamId, tmbId } = await authUserPer({ - req, - authToken: true, - authApiKey: true, - per: WritePermissionVal - }); - let collectionName = ''; await mongoSessionRun(async (session) => { diff --git a/projects/app/src/pages/api/core/evaluation/metric/create.ts b/projects/app/src/pages/api/core/evaluation/metric/create.ts index 36c09314850c..437496271f46 100644 --- a/projects/app/src/pages/api/core/evaluation/metric/create.ts +++ b/projects/app/src/pages/api/core/evaluation/metric/create.ts @@ -2,14 +2,17 @@ import type { ApiRequestProps, ApiResponseType } from '@fastgpt/service/type/nex import type { CreateMetricBody } from '@fastgpt/global/core/evaluation/metric/api'; import { NextAPI } from '@/service/middleware/entry'; import { MongoEvalMetric } from '@fastgpt/service/core/evaluation/metric/schema'; -import { authUserPer } from '@fastgpt/service/support/permission/user/auth'; -import { WritePermissionVal } from '@fastgpt/global/support/permission/constant'; import { EvalMetricTypeEnum } from '@fastgpt/global/core/evaluation/metric/constants'; -import { addAuditLog, getI18nAppType } from '@fastgpt/service/support/user/audit/util'; - +import { authEvaluationMetricCreate } from '@fastgpt/service/core/evaluation/common'; async function handler(req: ApiRequestProps, res: ApiResponseType) { const { name, description, prompt } = req.body; + const { teamId, tmbId } = await authEvaluationMetricCreate({ + req, + authApiKey: true, + authToken: true + }); + if (!name || typeof name !== 'string' || name.trim().length === 0) { return Promise.reject('Metric name is required and must be a non-empty string'); } @@ -34,13 +37,6 @@ async function handler(req: ApiRequestProps, res: ApiRespo return Promise.reject('Prompt must be less than 4000 characters'); } - const { teamId, tmbId } = await authUserPer({ - req, - authToken: true, - authApiKey: true, - per: WritePermissionVal - }); - const metric = await MongoEvalMetric.create({ teamId: teamId, tmbId: tmbId, diff --git a/projects/app/src/pages/api/core/evaluation/metric/delete.ts b/projects/app/src/pages/api/core/evaluation/metric/delete.ts index d4c5f0d8018f..566b97b6b654 100644 --- a/projects/app/src/pages/api/core/evaluation/metric/delete.ts +++ b/projects/app/src/pages/api/core/evaluation/metric/delete.ts @@ -2,25 +2,19 @@ import type { ApiRequestProps, ApiResponseType } from '@fastgpt/service/type/nex import { NextAPI } from '@/service/middleware/entry'; import { MongoEvalMetric } from '@fastgpt/service/core/evaluation/metric/schema'; import { EvalMetricTypeEnum } from '@fastgpt/global/core/evaluation/metric/constants'; -import { authUserPer } from '@fastgpt/service/support/permission/user/auth'; -import { WritePermissionVal } from '@fastgpt/global/support/permission/constant'; -import { addAuditLog, getI18nAppType } from '@fastgpt/service/support/user/audit/util'; -import { AuditEventEnum } from '@fastgpt/global/support/user/audit/constants'; - +import { authEvaluationMetricWrite } from '@fastgpt/service/core/evaluation/common'; type Query = { id: string }; async function handler(req: ApiRequestProps<{}, Query>, res: ApiResponseType) { const { id } = req.query; - if (!id) { return Promise.reject('Missing required parameter: id'); } - const { teamId, tmbId } = await authUserPer({ + const { teamId } = await authEvaluationMetricWrite(id, { req, - authToken: true, authApiKey: true, - per: WritePermissionVal + authToken: true }); const metric = await MongoEvalMetric.findById(id); diff --git a/projects/app/src/pages/api/core/evaluation/metric/detail.ts b/projects/app/src/pages/api/core/evaluation/metric/detail.ts index 7703b6020176..cf56a75d0616 100644 --- a/projects/app/src/pages/api/core/evaluation/metric/detail.ts +++ b/projects/app/src/pages/api/core/evaluation/metric/detail.ts @@ -1,24 +1,20 @@ import type { ApiRequestProps, ApiResponseType } from '@fastgpt/service/type/next'; import { NextAPI } from '@/service/middleware/entry'; import { MongoEvalMetric } from '@fastgpt/service/core/evaluation/metric/schema'; -import { authUserPer } from '@fastgpt/service/support/permission/user/auth'; -import { ReadPermissionVal } from '@fastgpt/global/support/permission/constant'; - +import { authEvaluationMetricRead } from '@fastgpt/service/core/evaluation/common'; type Query = { id: string }; async function handler(req: ApiRequestProps<{}, Query>, res: ApiResponseType) { const { id } = req.query; - if (!id) { - return Promise.reject('Missing required parameter: id'); - } - - await authUserPer({ + const { teamId } = await authEvaluationMetricRead(id, { req, - authToken: true, authApiKey: true, - per: ReadPermissionVal + authToken: true }); + if (!id) { + return Promise.reject('Missing required parameter: id'); + } const metric = await MongoEvalMetric.findById(id).lean(); if (!metric) { diff --git a/projects/app/src/pages/api/core/evaluation/metric/list.ts b/projects/app/src/pages/api/core/evaluation/metric/list.ts index 1959abce2eff..a5f533b84fcb 100644 --- a/projects/app/src/pages/api/core/evaluation/metric/list.ts +++ b/projects/app/src/pages/api/core/evaluation/metric/list.ts @@ -7,8 +7,11 @@ import { parsePaginationRequest } from '@fastgpt/service/common/api/pagination'; import { replaceRegChars } from '@fastgpt/global/common/string/tools'; import { Types } from '@fastgpt/service/common/mongo'; import { addSourceMember } from '@fastgpt/service/support/user/utils'; - +import { EvaluationPermission } from '@fastgpt/global/support/permission/evaluation/controller'; +import { sumPer } from '@fastgpt/global/support/permission/utils'; import type { ListMetricsBody } from '@fastgpt/global/core/evaluation/metric/api'; +import { addLog } from '@fastgpt/service/common/system/log'; +import { getEvaluationPermissionAggregation } from '@fastgpt/service/core/evaluation/common'; async function handler(req: ApiRequestProps) { const { teamId, tmbId } = await authUserPer({ @@ -30,26 +33,107 @@ async function handler(req: ApiRequestProps) { } try { + const { teamId, tmbId, isOwner, roleList, myGroupMap, myOrgSet } = + await getEvaluationPermissionAggregation({ + req, + authApiKey: true, + authToken: true + }); + + const myRoles = roleList.filter( + (item) => + String(item.tmbId) === String(tmbId) || + myGroupMap.has(String(item.groupId)) || + myOrgSet.has(String(item.orgId)) + ); + const accessibleIds = myRoles.map((item) => item.resourceId); + + const filter: any = { teamId: new Types.ObjectId(teamId) }; + if (searchKey) { + filter.$or = [ + { name: { $regex: searchKey, $options: 'i' } }, + { description: { $regex: searchKey, $options: 'i' } } + ]; + } + const limit = pageSize; + const sort = { createTime: -1 as const }; + + // If not owner, filter by accessible resources + let finalFilter = filter; + if (!isOwner && accessibleIds) { + finalFilter = { + ...filter, + $or: [ + { _id: { $in: accessibleIds.map((id) => new Types.ObjectId(id)) } }, + ...(tmbId ? [{ tmbId: new Types.ObjectId(tmbId) }] : []) // Own metrics + ] + }; + } + const [metrics, total] = await Promise.all([ - MongoEvalMetric.find(match).sort({ createTime: -1 }).skip(offset).limit(pageSize).lean(), - MongoEvalMetric.countDocuments(match) + MongoEvalMetric.find(finalFilter).sort(sort).skip(offset).limit(limit).lean(), + MongoEvalMetric.countDocuments(finalFilter) ]); - const listWithSourceMember = await addSourceMember({ - list: metrics.map((item: any) => ({ - _id: String(item._id), - name: item.name, - description: item.description || '', - createTime: item.createTime, - updateTime: item.updateTime, - tmbId: item.tmbId - })) + const formatMetrics = metrics + .map((metric: any) => { + const getPer = (metricId: string) => { + const tmbRole = myRoles.find( + (item) => String(item.resourceId) === metricId && !!item.tmbId + )?.permission; + const groupRole = sumPer( + ...myRoles + .filter( + (item) => String(item.resourceId) === metricId && (!!item.groupId || !!item.orgId) + ) + .map((item) => item.permission) + ); + return new EvaluationPermission({ + role: tmbRole ?? groupRole, + isOwner: String(metric.tmbId) === String(tmbId) || isOwner + }); + }; + + const getClbCount = (metricId: string) => { + return roleList.filter((item) => String(item.resourceId) === String(metricId)).length; + }; + + const getPrivateStatus = (metricId: string) => { + const collaboratorCount = getClbCount(metricId); + if (isOwner) { + return collaboratorCount <= 1; + } + return ( + collaboratorCount === 0 || + (collaboratorCount === 1 && String(metric.tmbId) === String(tmbId)) + ); + }; + + return { + ...metric, + permission: getPer(String(metric._id)), + private: getPrivateStatus(String(metric._id)) + }; + }) + .filter((metric: any) => metric.permission.hasReadPer); + + const formattedResult = await addSourceMember({ + list: formatMetrics }); - return { - total, - list: listWithSourceMember + const finalResult = { + list: formattedResult, + total: total }; + + addLog.info('[Evaluation Metric] Metric list query successful', { + pageSize: pageSize, + searchKey: searchKey?.trim(), + total: finalResult.total, + returned: finalResult.list.length + }); + + return finalResult; } catch (error) { return Promise.reject('Failed to fetch evaluation metrics'); } diff --git a/projects/app/src/pages/api/core/evaluation/metric/update.ts b/projects/app/src/pages/api/core/evaluation/metric/update.ts index bb7a5fe79686..9cfce9e3e306 100644 --- a/projects/app/src/pages/api/core/evaluation/metric/update.ts +++ b/projects/app/src/pages/api/core/evaluation/metric/update.ts @@ -3,14 +3,14 @@ import { NextAPI } from '@/service/middleware/entry'; import { MongoEvalMetric } from '@fastgpt/service/core/evaluation/metric/schema'; import type { UpdateMetricBody } from '@fastgpt/global/core/evaluation/metric/api'; import { EvalMetricTypeEnum } from '@fastgpt/global/core/evaluation/metric/constants'; -import { authUserPer } from '@fastgpt/service/support/permission/user/auth'; -import { WritePermissionVal } from '@fastgpt/global/support/permission/constant'; -import { addAuditLog, getI18nAppType } from '@fastgpt/service/support/user/audit/util'; -import { AuditEventEnum } from '@fastgpt/global/support/user/audit/constants'; - +import { authEvaluationMetricWrite } from '@fastgpt/service/core/evaluation/common'; async function handler(req: ApiRequestProps, res: ApiResponseType) { const { id, name, description, prompt } = req.body; - + const { teamId } = await authEvaluationMetricWrite(id, { + req, + authApiKey: true, + authToken: true + }); if (!id) { return Promise.reject('Missing required parameter: id'); } @@ -39,13 +39,6 @@ async function handler(req: ApiRequestProps, res: ApiRespo return Promise.reject('Prompt must be less than 4000 characters'); } - const { teamId, tmbId } = await authUserPer({ - req, - authToken: true, - authApiKey: true, - per: WritePermissionVal - }); - const metric = await MongoEvalMetric.findById(id); if (!metric) { return Promise.reject('Metric not found'); diff --git a/projects/app/src/pages/api/core/evaluation/task/create.ts b/projects/app/src/pages/api/core/evaluation/task/create.ts index b436217a748e..59282ab856fd 100644 --- a/projects/app/src/pages/api/core/evaluation/task/create.ts +++ b/projects/app/src/pages/api/core/evaluation/task/create.ts @@ -9,6 +9,7 @@ import type { } from '@fastgpt/global/core/evaluation/api'; import { validateTargetConfig } from '@fastgpt/service/core/evaluation/target'; import { validateEvaluationParams } from '@fastgpt/global/core/evaluation/utils'; +import { authEvaluationTaskCreate } from '@fastgpt/service/core/evaluation/common'; async function handler( req: ApiRequestProps @@ -45,20 +46,21 @@ async function handler( } } - // Create evaluation task - const evaluation = await EvaluationTaskService.createEvaluation( - { - name: name.trim(), - description: description?.trim(), - datasetId, - target: target as EvalTarget, - evaluators - }, - { - req, - authToken: true - } - ); + const { teamId, tmbId } = await authEvaluationTaskCreate(target as EvalTarget, { + req, + authApiKey: true, + authToken: true + }); + + const evaluation = await EvaluationTaskService.createEvaluation({ + name: name.trim(), + description: description?.trim(), + datasetId, + target: target as EvalTarget, + evaluators, + teamId, + tmbId + }); addLog.info('[Evaluation] Evaluation task created successfully', { evalId: evaluation._id, diff --git a/projects/app/src/pages/api/core/evaluation/task/delete.ts b/projects/app/src/pages/api/core/evaluation/task/delete.ts index 27047be2762d..68d6a487cf7e 100644 --- a/projects/app/src/pages/api/core/evaluation/task/delete.ts +++ b/projects/app/src/pages/api/core/evaluation/task/delete.ts @@ -6,6 +6,7 @@ import type { DeleteEvaluationResponse } from '@fastgpt/global/core/evaluation/api'; import { addLog } from '@fastgpt/service/common/system/log'; +import { authEvaluationTaskWrite } from '@fastgpt/service/core/evaluation/common'; async function handler( req: ApiRequestProps<{}, DeleteEvaluationRequest> @@ -17,11 +18,14 @@ async function handler( return Promise.reject('Evaluation ID is required'); } - await EvaluationTaskService.deleteEvaluation(evalId, { + const { teamId } = await authEvaluationTaskWrite(evalId, { req, + authApiKey: true, authToken: true }); + await EvaluationTaskService.deleteEvaluation(evalId, teamId); + addLog.info('[Evaluation] Evaluation task deleted successfully', { evalId: evalId }); diff --git a/projects/app/src/pages/api/core/evaluation/task/detail.ts b/projects/app/src/pages/api/core/evaluation/task/detail.ts index f2ec9ddb44f2..f3671cd0769a 100644 --- a/projects/app/src/pages/api/core/evaluation/task/detail.ts +++ b/projects/app/src/pages/api/core/evaluation/task/detail.ts @@ -6,6 +6,7 @@ import type { EvaluationDetailResponse } from '@fastgpt/global/core/evaluation/api'; import { addLog } from '@fastgpt/service/common/system/log'; +import { authEvaluationTaskRead } from '@fastgpt/service/core/evaluation/common'; async function handler( req: ApiRequestProps<{}, EvaluationDetailRequest> @@ -17,11 +18,15 @@ async function handler( return Promise.reject('Evaluation ID is required'); } - const evaluation = await EvaluationTaskService.getEvaluation(evalId, { + const { evaluation } = await authEvaluationTaskRead(evalId, { req, + authApiKey: true, authToken: true }); + // optional, if need addition handle + // const evaluationDetails = await EvaluationTaskService.getEvaluation(evalId); + addLog.info('[Evaluation] Evaluation task details retrieved successfully', { evalId: evalId, name: evaluation.name, diff --git a/projects/app/src/pages/api/core/evaluation/task/item/delete.ts b/projects/app/src/pages/api/core/evaluation/task/item/delete.ts index 8daabad27cfc..4f8d49c4c527 100644 --- a/projects/app/src/pages/api/core/evaluation/task/item/delete.ts +++ b/projects/app/src/pages/api/core/evaluation/task/item/delete.ts @@ -6,6 +6,7 @@ import type { DeleteEvaluationItemResponse } from '@fastgpt/global/core/evaluation/api'; import { addLog } from '@fastgpt/service/common/system/log'; +import { authEvaluationItemWrite } from '@fastgpt/service/core/evaluation/common'; async function handler( req: ApiRequestProps<{}, DeleteEvaluationItemRequest> @@ -17,11 +18,14 @@ async function handler( return Promise.reject('Evaluation item ID is required'); } - await EvaluationTaskService.deleteEvaluationItem(evalItemId, { + const { teamId } = await authEvaluationItemWrite(evalItemId, { req, + authApiKey: true, authToken: true }); + await EvaluationTaskService.deleteEvaluationItem(evalItemId, teamId); + addLog.info('[Evaluation] Evaluation item deleted successfully', { evalItemId }); diff --git a/projects/app/src/pages/api/core/evaluation/task/item/detail.ts b/projects/app/src/pages/api/core/evaluation/task/item/detail.ts index 7d67884c8569..aa83b2e4f103 100644 --- a/projects/app/src/pages/api/core/evaluation/task/item/detail.ts +++ b/projects/app/src/pages/api/core/evaluation/task/item/detail.ts @@ -6,6 +6,7 @@ import type { EvaluationItemDetailResponse } from '@fastgpt/global/core/evaluation/api'; import { addLog } from '@fastgpt/service/common/system/log'; +import { authEvaluationItemRead } from '@fastgpt/service/core/evaluation/common'; async function handler( req: ApiRequestProps<{}, EvaluationItemDetailRequest> @@ -17,11 +18,15 @@ async function handler( return Promise.reject('Evaluation item ID is required'); } - const result = await EvaluationTaskService.getEvaluationItemResult(evalItemId, { + const { teamId } = await authEvaluationItemRead(evalItemId, { req, + authApiKey: true, authToken: true }); + // Service层业务逻辑 + const result = await EvaluationTaskService.getEvaluationItemResult(evalItemId, teamId); + addLog.info('[Evaluation] Evaluation item details query successful', { evalItemId: evalItemId, hasResponse: !!result.response, diff --git a/projects/app/src/pages/api/core/evaluation/task/item/export.ts b/projects/app/src/pages/api/core/evaluation/task/item/export.ts index 19c7feb62c32..da65654e2921 100644 --- a/projects/app/src/pages/api/core/evaluation/task/item/export.ts +++ b/projects/app/src/pages/api/core/evaluation/task/item/export.ts @@ -3,6 +3,7 @@ import { NextAPI } from '@/service/middleware/entry'; import { EvaluationTaskService } from '@fastgpt/service/core/evaluation/task'; import type { ExportEvaluationItemsRequest } from '@fastgpt/global/core/evaluation/api'; import { addLog } from '@fastgpt/service/common/system/log'; +import { authEvaluationTaskRead } from '@fastgpt/service/core/evaluation/common'; async function handler( req: ApiRequestProps<{}, ExportEvaluationItemsRequest>, @@ -19,12 +20,15 @@ async function handler( return Promise.reject('Format must be json or csv'); } + const { teamId } = await authEvaluationTaskRead(evalId, { + req, + authApiKey: true, + authToken: true + }); + const results = await EvaluationTaskService.exportEvaluationResults( evalId, - { - req, - authToken: true - }, + teamId, format as 'json' | 'csv' ); diff --git a/projects/app/src/pages/api/core/evaluation/task/item/list.ts b/projects/app/src/pages/api/core/evaluation/task/item/list.ts index 7b69095ac593..fc30f9790fb9 100644 --- a/projects/app/src/pages/api/core/evaluation/task/item/list.ts +++ b/projects/app/src/pages/api/core/evaluation/task/item/list.ts @@ -6,6 +6,7 @@ import type { ListEvaluationItemsResponse } from '@fastgpt/global/core/evaluation/api'; import { addLog } from '@fastgpt/service/common/system/log'; +import { authEvaluationTaskRead } from '@fastgpt/service/core/evaluation/common'; async function handler( req: ApiRequestProps @@ -28,12 +29,15 @@ async function handler( return Promise.reject('Invalid page size (1-100)'); } + const { teamId } = await authEvaluationTaskRead(evalId, { + req, + authApiKey: true, + authToken: true + }); + const result = await EvaluationTaskService.listEvaluationItems( evalId, - { - req, - authToken: true - }, + teamId, pageNumInt, pageSizeInt ); diff --git a/projects/app/src/pages/api/core/evaluation/task/item/retry.ts b/projects/app/src/pages/api/core/evaluation/task/item/retry.ts index 6f73a1217004..e62f15a8b8a3 100644 --- a/projects/app/src/pages/api/core/evaluation/task/item/retry.ts +++ b/projects/app/src/pages/api/core/evaluation/task/item/retry.ts @@ -6,6 +6,7 @@ import type { RetryEvaluationItemResponse } from '@fastgpt/global/core/evaluation/api'; import { addLog } from '@fastgpt/service/common/system/log'; +import { authEvaluationItemWrite } from '@fastgpt/service/core/evaluation/common'; async function handler( req: ApiRequestProps @@ -17,11 +18,14 @@ async function handler( return Promise.reject('Evaluation item ID is required'); } - await EvaluationTaskService.retryEvaluationItem(evalItemId, { + const { teamId } = await authEvaluationItemWrite(evalItemId, { req, + authApiKey: true, authToken: true }); + await EvaluationTaskService.retryEvaluationItem(evalItemId, teamId); + addLog.info('[Evaluation] Evaluation item retry started successfully', { evalItemId }); diff --git a/projects/app/src/pages/api/core/evaluation/task/item/update.ts b/projects/app/src/pages/api/core/evaluation/task/item/update.ts index 9e6905021ffa..5825acec32a6 100644 --- a/projects/app/src/pages/api/core/evaluation/task/item/update.ts +++ b/projects/app/src/pages/api/core/evaluation/task/item/update.ts @@ -9,12 +9,19 @@ import type { UpdateEvaluationItemResponse } from '@fastgpt/global/core/evaluation/api'; import { addLog } from '@fastgpt/service/common/system/log'; +import { authEvaluationItemWrite } from '@fastgpt/service/core/evaluation/common'; async function handler( req: ApiRequestProps ): Promise { try { - const { evalItemId, userInput, expectedOutput } = req.body; + const { evalItemId, userInput, expectedOutput, variables } = req.body; + + const { teamId } = await authEvaluationItemWrite(evalItemId, { + req, + authApiKey: true, + authToken: true + }); if (!evalItemId) { return Promise.reject('Evaluation item ID is required'); @@ -35,14 +42,11 @@ async function handler( updates.dataItem = dataItemUpdates as EvalDatasetDataSchemaType; } - await EvaluationTaskService.updateEvaluationItem(evalItemId, updates, { - req, - authToken: true - }); + await EvaluationTaskService.updateEvaluationItem(evalItemId, updates, teamId); addLog.info('[Evaluation] Evaluation item updated successfully', { evalItemId, - updates: { userInput, expectedOutput } + updates: { userInput, expectedOutput, variables } }); return { message: 'Evaluation item updated successfully' }; diff --git a/projects/app/src/pages/api/core/evaluation/task/list.ts b/projects/app/src/pages/api/core/evaluation/task/list.ts index 6f50f7f9527c..ffa80cdc7a07 100644 --- a/projects/app/src/pages/api/core/evaluation/task/list.ts +++ b/projects/app/src/pages/api/core/evaluation/task/list.ts @@ -5,7 +5,12 @@ import type { ListEvaluationsRequest, ListEvaluationsResponse } from '@fastgpt/global/core/evaluation/api'; +import { EvaluationPermission } from '@fastgpt/global/support/permission/evaluation/controller'; +import { sumPer } from '@fastgpt/global/support/permission/utils'; +import { addSourceMember } from '@fastgpt/service/support/user/utils'; + import { addLog } from '@fastgpt/service/common/system/log'; +import { getEvaluationPermissionAggregation } from '@fastgpt/service/core/evaluation/common'; async function handler( req: ApiRequestProps @@ -13,7 +18,6 @@ async function handler( try { const { pageNum = 1, pageSize = 20, searchKey } = req.body; - // Validate pagination parameters const pageNumInt = Number(pageNum); const pageSizeInt = Number(pageSize); @@ -25,28 +29,95 @@ async function handler( return Promise.reject('Invalid page size (1-100)'); } - const result = await EvaluationTaskService.listEvaluations( - { + const { teamId, tmbId, isOwner, roleList, myGroupMap, myOrgSet } = + await getEvaluationPermissionAggregation({ req, + authApiKey: true, authToken: true - }, + }); + + const myRoles = roleList.filter( + (item) => + String(item.tmbId) === String(tmbId) || + myGroupMap.has(String(item.groupId)) || + myOrgSet.has(String(item.orgId)) + ); + const accessibleIds = myRoles.map((item) => item.resourceId); + + const result = await EvaluationTaskService.listEvaluations( + teamId, pageNumInt, pageSizeInt, - searchKey?.trim() + searchKey?.trim(), + accessibleIds, + tmbId, + isOwner ); + const formatEvaluations = result.list + .map((evaluation: any) => { + const getPer = (evalId: string) => { + const tmbRole = myRoles.find( + (item) => String(item.resourceId) === evalId && !!item.tmbId + )?.permission; + const groupRole = sumPer( + ...myRoles + .filter( + (item) => String(item.resourceId) === evalId && (!!item.groupId || !!item.orgId) + ) + .map((item) => item.permission) + ); + + return new EvaluationPermission({ + role: tmbRole ?? groupRole, + isOwner: String(evaluation.tmbId) === String(tmbId) || isOwner + }); + }; + + const getClbCount = (evalId: string) => { + return roleList.filter((item) => String(item.resourceId) === String(evalId)).length; + }; + + const getPrivateStatus = (evalId: string) => { + const collaboratorCount = getClbCount(evalId); + // 参照app list逻辑:协作者数量 <= 1 且非团队owner时为私有 + // 团队owner可以看到所有评估的协作状态 + if (isOwner) { + return collaboratorCount <= 1; + } + // 普通用户:无协作者或只有自己为私有 + return ( + collaboratorCount === 0 || + (collaboratorCount === 1 && String(evaluation.tmbId) === String(tmbId)) + ); + }; + + return { + ...evaluation, + permission: getPer(String(evaluation._id)), + private: getPrivateStatus(String(evaluation._id)) + }; + }) + .filter((evaluation: any) => evaluation.permission.hasReadPer); + + const formattedResult = await addSourceMember({ + list: formatEvaluations + }); + + const finalResult = { + list: formattedResult, + total: result.total + }; + addLog.info('[Evaluation] Evaluation list query successful', { pageNum: pageNumInt, pageSize: pageSizeInt, searchKey: searchKey?.trim(), - total: result.total, - returned: result.evaluations.length + total: finalResult.total, + returned: finalResult.list.length }); - return { - list: result.evaluations, - total: result.total - }; + return finalResult; } catch (error) { addLog.error('[Evaluation] Failed to query evaluation list', error); return Promise.reject(error); diff --git a/projects/app/src/pages/api/core/evaluation/task/retryFailed.ts b/projects/app/src/pages/api/core/evaluation/task/retryFailed.ts index 177193febffc..06317365d7eb 100644 --- a/projects/app/src/pages/api/core/evaluation/task/retryFailed.ts +++ b/projects/app/src/pages/api/core/evaluation/task/retryFailed.ts @@ -6,6 +6,7 @@ import type { RetryFailedEvaluationItemsRequest, RetryFailedItemsResponse } from '@fastgpt/global/core/evaluation/api'; +import { authEvaluationTaskWrite } from '@fastgpt/service/core/evaluation/common'; async function handler( req: ApiRequestProps @@ -17,11 +18,14 @@ async function handler( return Promise.reject('Evaluation ID is required'); } - const retryCount = await EvaluationTaskService.retryFailedItems(evalId, { + const { teamId } = await authEvaluationTaskWrite(evalId, { req, + authApiKey: true, authToken: true }); + const retryCount = await EvaluationTaskService.retryFailedItems(evalId, teamId); + addLog.info('[Evaluation] Failed items retry batch started successfully', { evalId, retryCount diff --git a/projects/app/src/pages/api/core/evaluation/task/start.ts b/projects/app/src/pages/api/core/evaluation/task/start.ts index 7888d4b9a0c7..83ee124c38ae 100644 --- a/projects/app/src/pages/api/core/evaluation/task/start.ts +++ b/projects/app/src/pages/api/core/evaluation/task/start.ts @@ -1,4 +1,4 @@ -import type { ApiRequestProps, ApiResponseType } from '@fastgpt/service/type/next'; +import type { ApiRequestProps } from '@fastgpt/service/type/next'; import { NextAPI } from '@/service/middleware/entry'; import { EvaluationTaskService } from '@fastgpt/service/core/evaluation/task'; import type { @@ -6,6 +6,7 @@ import type { StartEvaluationResponse } from '@fastgpt/global/core/evaluation/api'; import { addLog } from '@fastgpt/service/common/system/log'; +import { authEvaluationTaskExecution } from '@fastgpt/service/core/evaluation/common'; async function handler( req: ApiRequestProps @@ -17,11 +18,14 @@ async function handler( return Promise.reject('Evaluation ID is required'); } - await EvaluationTaskService.startEvaluation(evalId, { + const { teamId } = await authEvaluationTaskExecution(evalId, { req, + authApiKey: true, authToken: true }); + await EvaluationTaskService.startEvaluation(evalId, teamId); + addLog.info('[Evaluation] Evaluation task started successfully', { evalId }); diff --git a/projects/app/src/pages/api/core/evaluation/task/stats.ts b/projects/app/src/pages/api/core/evaluation/task/stats.ts index 167dd194776c..1494c33781c9 100644 --- a/projects/app/src/pages/api/core/evaluation/task/stats.ts +++ b/projects/app/src/pages/api/core/evaluation/task/stats.ts @@ -6,6 +6,7 @@ import type { EvaluationStatsResponse } from '@fastgpt/global/core/evaluation/api'; import { addLog } from '@fastgpt/service/common/system/log'; +import { authEvaluationTaskRead } from '@fastgpt/service/core/evaluation/common'; async function handler( req: ApiRequestProps<{}, StatsEvaluationRequest> @@ -17,11 +18,14 @@ async function handler( return Promise.reject('Evaluation ID is required'); } - const stats = await EvaluationTaskService.getEvaluationStats(evalId, { + const { teamId } = await authEvaluationTaskRead(evalId, { req, + authApiKey: true, authToken: true }); + const stats = await EvaluationTaskService.getEvaluationStats(evalId, teamId); + addLog.info('[Evaluation] Evaluation task statistics query successful', { evalId, total: stats.total, diff --git a/projects/app/src/pages/api/core/evaluation/task/stop.ts b/projects/app/src/pages/api/core/evaluation/task/stop.ts index 8a55236e2d09..edcd51fec7ab 100644 --- a/projects/app/src/pages/api/core/evaluation/task/stop.ts +++ b/projects/app/src/pages/api/core/evaluation/task/stop.ts @@ -6,6 +6,7 @@ import type { StopEvaluationResponse } from '@fastgpt/global/core/evaluation/api'; import { addLog } from '@fastgpt/service/common/system/log'; +import { authEvaluationTaskExecution } from '@fastgpt/service/core/evaluation/common'; async function handler( req: ApiRequestProps @@ -17,11 +18,14 @@ async function handler( return Promise.reject('Evaluation ID is required'); } - await EvaluationTaskService.stopEvaluation(evalId, { + const { teamId } = await authEvaluationTaskExecution(evalId, { req, + authApiKey: true, authToken: true }); + await EvaluationTaskService.stopEvaluation(evalId, teamId); + addLog.info('[Evaluation] Evaluation task stopped successfully', { evalId }); diff --git a/projects/app/src/pages/api/core/evaluation/task/update.ts b/projects/app/src/pages/api/core/evaluation/task/update.ts index e56238a31cde..d23f1473bad5 100644 --- a/projects/app/src/pages/api/core/evaluation/task/update.ts +++ b/projects/app/src/pages/api/core/evaluation/task/update.ts @@ -7,6 +7,7 @@ import type { } from '@fastgpt/global/core/evaluation/api'; import { addLog } from '@fastgpt/service/common/system/log'; import { validateEvaluationParams } from '@fastgpt/global/core/evaluation/utils'; +import { authEvaluationTaskWrite } from '@fastgpt/service/core/evaluation/common'; async function handler( req: ApiRequestProps @@ -27,16 +28,19 @@ async function handler( return Promise.reject(paramValidation.message); } + const { teamId } = await authEvaluationTaskWrite(evalId, { + req, + authApiKey: true, + authToken: true + }); + await EvaluationTaskService.updateEvaluation( evalId, { ...(name !== undefined && { name: name.trim() }), ...(description !== undefined && { description: description?.trim() }) }, - { - req, - authToken: true - } + teamId ); addLog.info('[Evaluation] Evaluation task updated successfully', { diff --git a/projects/app/src/pages/dashboard/evaluation/components/OldEvaluationTasks.tsx b/projects/app/src/pages/dashboard/evaluation/components/OldEvaluationTasks.tsx deleted file mode 100644 index cc78887ec603..000000000000 --- a/projects/app/src/pages/dashboard/evaluation/components/OldEvaluationTasks.tsx +++ /dev/null @@ -1,241 +0,0 @@ -import { - Box, - Button, - Flex, - IconButton, - Table, - TableContainer, - Tbody, - Td, - Th, - Thead, - Tr -} from '@chakra-ui/react'; -import SearchInput from '@fastgpt/web/components/common/Input/SearchInput'; -import MyIcon from '@fastgpt/web/components/common/Icon'; -import { useRouter } from 'next/router'; -import { useRequest2 } from '@fastgpt/web/hooks/useRequest'; -import { deleteEvaluation, getEvaluationList } from '@/web/core/evaluation/evaluation'; -import { formatTime2YMDHM } from '@fastgpt/global/common/string/time'; -import Avatar from '@fastgpt/web/components/common/Avatar'; -import { usePagination } from '@fastgpt/web/hooks/usePagination'; -import { useState, useEffect, useMemo } from 'react'; -import EvaluationDetailModal from '@/pageComponents/dashboard/evaluation/DetailModal'; -import { useSystem } from '@fastgpt/web/hooks/useSystem'; -import EmptyTip from '@fastgpt/web/components/common/EmptyTip'; -// import type { evaluationType } from '@fastgpt/global/core/evaluation/type'; -import MyTooltip from '@fastgpt/web/components/common/MyTooltip'; -import PopoverConfirm from '@fastgpt/web/components/common/MyPopover/PopoverConfirm'; -import MyBox from '@fastgpt/web/components/common/MyBox'; -import { useTranslation } from 'next-i18next'; - -type evaluationType = any; - -const EvaluationTasks = () => { - const router = useRouter(); - const { t } = useTranslation(); - const { isPc } = useSystem(); - - const [searchKey, setSearchKey] = useState(''); - const [evalDetailId, setEvalDetailId] = useState(); - const [pollingInterval, setPollingInterval] = useState(10000); - - const { - data: evaluationList, - Pagination, - getData: fetchData - } = usePagination(getEvaluationList, { - defaultPageSize: 20, - pollingInterval, - pollingWhenHidden: true, - params: { - searchKey - }, - EmptyTip: , - refreshDeps: [searchKey] - }); - - const evalDetail = useMemo(() => { - if (!evalDetailId) return undefined; - return evaluationList.find((item) => item._id === evalDetailId); - }, [evalDetailId, evaluationList]); - - useEffect(() => { - const hasRunningOrErrorTasks = evaluationList.some((item) => { - const { totalCount = 0, completedCount = 0, errorCount = 0 } = item; - const isCompleted = totalCount === completedCount; - return !isCompleted || errorCount > 0; - }); - - setPollingInterval(hasRunningOrErrorTasks ? 10000 : 0); - }, [evaluationList]); - - const { runAsync: onDeleteEval } = useRequest2(deleteEvaluation, { - onSuccess: () => { - fetchData(); - } - }); - - const renderProgress = (item: evaluationType) => { - const { completedCount, totalCount, errorCount } = item; - - if (completedCount === totalCount) { - return ( - - {t('dashboard_evaluation:completed')} - - ); - } - - return ( - - {completedCount} - {`/${totalCount}`} - {(errorCount > 0 || item.errorMessage) && ( - - setEvalDetailId(item._id)} - /> - - )} - - ); - }; - - return ( - <> - - - - { - setSearchKey(e.target.value); - }} - /> - - - - - - - - - - - - - - - - - - - - - {evaluationList.map((item) => { - return ( - - - - - - - - - - ); - })} - -
{t('dashboard_evaluation:Task_name')}{t('dashboard_evaluation:Progress')}{t('dashboard_evaluation:Executor')}{t('dashboard_evaluation:Evaluation_app')}{t('dashboard_evaluation:Start_end_time')}{t('dashboard_evaluation:Overall_score')}{t('dashboard_evaluation:Action')}
- {item.name} - {renderProgress(item)} - - - {item.executorName} - - - - - {item.appName} - - - {formatTime2YMDHM(item.createTime)} - {formatTime2YMDHM(item.finishTime)} - - {typeof item.score === 'number' ? (item.score * 100).toFixed(2) : '-'} - - - - } - /> - } - content={t('dashboard_evaluation:comfirm_delete_task')} - onConfirm={() => onDeleteEval({ evalId: item._id })} - /> -
-
-
- - - - - - {!!evalDetail && ( - setEvalDetailId(undefined)} - fetchEvalList={() => fetchData()} - /> - )} - - ); -}; - -export default EvaluationTasks; diff --git a/projects/app/src/pages/dashboard/evaluation/components/create.tsx b/projects/app/src/pages/dashboard/evaluation/components/create.tsx deleted file mode 100644 index c24006dd7d72..000000000000 --- a/projects/app/src/pages/dashboard/evaluation/components/create.tsx +++ /dev/null @@ -1,384 +0,0 @@ -import MyBox from '@fastgpt/web/components/common/MyBox'; -import DashboardContainer from '../../../../pageComponents/dashboard/Container'; -import { useTranslation } from 'next-i18next'; -import { Box, Button, Flex, Input, VStack } from '@chakra-ui/react'; -import { useRouter } from 'next/router'; -import { serviceSideProps } from '@/web/common/i18n/utils'; -import AIModelSelector from '@/components/Select/AIModelSelector'; -import { useForm } from 'react-hook-form'; -import { useSystemStore } from '@/web/common/system/useSystemStore'; -import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel'; -import AppSelect from '@/components/Select/AppSelect'; -import MyIcon from '@fastgpt/web/components/common/Icon'; -import FileSelector, { - type SelectFileItemType -} from '@/pageComponents/dataset/detail/components/FileSelector'; -import { Trans } from 'next-i18next'; -import MyIconButton from '@fastgpt/web/components/common/Icon/button'; -import { useRequest2 } from '@fastgpt/web/hooks/useRequest'; -import { getAppDetailById } from '@/web/core/app/api'; -import { useToast } from '@fastgpt/web/hooks/useToast'; -import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip'; -import { fileDownload } from '@/web/common/file/utils'; -import { postCreateEvaluation } from '@/web/core/evaluation/evaluation'; -import { useMemo, useState } from 'react'; -import Markdown from '@/components/Markdown'; -// import { getEvaluationFileHeader } from '@fastgpt/global/core/evaluation/utils'; -import { evaluationFileErrors } from '@fastgpt/global/core/evaluation/constants'; -import { TeamErrEnum } from '@fastgpt/global/common/error/code/team'; -import { getErrText } from '@fastgpt/global/common/error/utils'; - -type EvaluationFormType = { - name: string; - evalModel: string; - appId: string; - evaluationFiles: SelectFileItemType[]; -}; - -const EvaluationCreating = () => { - const { t } = useTranslation(); - const router = useRouter(); - const { toast } = useToast(); - - const [percent, setPercent] = useState(0); - const [error, setError] = useState(); - - const { llmModelList, feConfigs } = useSystemStore(); - - const evalModelList = useMemo(() => { - return llmModelList.filter((item) => item.useInEvaluation); - }, [llmModelList]); - const { register, setValue, watch, handleSubmit } = useForm({ - defaultValues: { - name: '', - evalModel: evalModelList[0]?.model, - appId: '', - evaluationFiles: [] as SelectFileItemType[] - } - }); - - const name = watch('name'); - const evalModel = watch('evalModel'); - const appId = watch('appId'); - const evaluationFiles = watch('evaluationFiles'); - - const { runAsync: getAppDetail, loading: isLoadingAppDetail } = useRequest2(() => { - if (appId) return getAppDetailById(appId); - return Promise.resolve(null); - }); - - const handleDownloadTemplate = async () => { - // const appDetail = await getAppDetail(); - // const variables = appDetail?.chatConfig.variables; - // const templateContent = getEvaluationFileHeader(variables); - - // fileDownload({ - // text: templateContent, - // type: 'text/csv;charset=utf-8', - // filename: `${appDetail?.name}_evaluation.csv` - // }); - - // 临时禁用模板下载功能 - toast({ - title: t('模板下载功能暂时不可用'), - status: 'warning' - }); - }; - - const { runAsync: createEvaluation, loading: isCreating } = useRequest2( - async (data: EvaluationFormType) => { - await postCreateEvaluation({ - file: data.evaluationFiles[0].file, - name: data.name, - evalModel: data.evalModel, - appId: data.appId, - percentListen: setPercent - }); - }, - { - onSuccess: () => { - toast({ - title: t('dashboard_evaluation:evaluation_created'), - status: 'success' - }); - - router.push('/dashboard/evaluation'); - }, - errorToast: '', - onError: (error) => { - if (error.message === evaluationFileErrors) { - setError(error.message); - } else if (error.message === TeamErrEnum.aiPointsNotEnough) { - useSystemStore.getState().setNotSufficientModalType(error.message); - } else { - toast({ - title: t(getErrText(error)), - status: 'error' - }); - } - } - } - ); - - const onSubmit = async (data: EvaluationFormType) => { - if (!data.appId) { - return toast({ - title: t('dashboard_evaluation:app_required'), - status: 'warning' - }); - } - if (!data.evaluationFiles || data.evaluationFiles.length === 0) { - return toast({ - title: t('dashboard_evaluation:file_required'), - status: 'warning' - }); - } - - await createEvaluation(data); - }; - - return ( - - {() => ( - - - - - - {t('dashboard_evaluation:Task_name')} - - - - - - {t('dashboard_evaluation:Evaluation_model')} - - ({ - label: item.name, - value: item.model - }))} - onChange={(e) => { - setValue('evalModel', e); - }} - /> - - - - {t('dashboard_evaluation:Evaluation_app')} - - - - { - setValue('appId', id); - }} - /> - {appId && ( - - )} - - - - - {t('dashboard_evaluation:Evaluation_file')} - - {appId ? ( - - { - setValue('evaluationFiles', e); - }} - FileTypeNode={ - - - }} - /> - - } - /> - {evaluationFiles && evaluationFiles.length > 0 && ( - - {evaluationFiles.map((item, index) => ( - - - - {item.name} - - - { - setValue( - 'evaluationFiles', - evaluationFiles.filter((_, i) => i !== index) - ); - - setError(undefined); - }} - /> - - ))} - - )} - {error && ( - - - - {t('dashboard_evaluation:check_format')} - - - {t('dashboard_evaluation:check_error')} - - - - - )} - - ) : ( - - {t('dashboard_evaluation:app_required')} - - )} - - - - - - - )} - - ); -}; - -export default EvaluationCreating; - -export async function getServerSideProps(content: any) { - return { - props: { - ...(await serviceSideProps(content, ['dashboard_evaluation', 'file'])) - } - }; -} diff --git a/projects/app/src/pages/dashboard/evaluation/dataset/detail/index.tsx b/projects/app/src/pages/dashboard/evaluation/dataset/detail/index.tsx deleted file mode 100644 index 6f02b0f9cf01..000000000000 --- a/projects/app/src/pages/dashboard/evaluation/dataset/detail/index.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import React from 'react'; -import { Box, Text } from '@chakra-ui/react'; -import { useTranslation } from 'next-i18next'; - -const DatasetEvaluationDetail = () => { - const { t } = useTranslation(); - - return ( - - - dataset detail - - - ); -}; - -export default DatasetEvaluationDetail; diff --git a/projects/app/src/pages/dashboard/evaluation/dataset/fileImport.tsx b/projects/app/src/pages/dashboard/evaluation/dataset/fileImport.tsx deleted file mode 100644 index 1db2d572ef3e..000000000000 --- a/projects/app/src/pages/dashboard/evaluation/dataset/fileImport.tsx +++ /dev/null @@ -1,325 +0,0 @@ -import React, { useState, useCallback, useMemo } from 'react'; -import MyBox from '@fastgpt/web/components/common/MyBox'; -import DashboardContainer from '../../../../pageComponents/dashboard/Container'; -import { useTranslation } from 'next-i18next'; -import { Box, Button, Flex, Input, VStack, IconButton, Switch } from '@chakra-ui/react'; -import { useRouter } from 'next/router'; -import { serviceSideProps } from '@/web/common/i18n/utils'; -import { useForm } from 'react-hook-form'; -import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel'; -import MyIcon from '@fastgpt/web/components/common/Icon'; -import { useToast } from '@fastgpt/web/hooks/useToast'; -import AIModelSelector from '@/components/Select/AIModelSelector'; -import { useSystemStore } from '@/web/common/system/useSystemStore'; -import FileSelector, { - type SelectFileItemType, - type EvaluationFileItemType -} from '@/pageComponents/dashboard/evaluation/dataset/FileSelector'; -import RenderFiles from '@/pageComponents/dashboard/evaluation/dataset/RenderFiles'; -import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip'; -import { fileDownload } from '@/web/common/file/utils'; -import { useRequest2 } from '@fastgpt/web/hooks/useRequest'; -import { uploadFile2DB } from '@/web/common/file/controller'; -import { BucketNameEnum } from '@fastgpt/global/common/file/constants'; -import { getErrText } from '@fastgpt/global/common/error/utils'; -import { formatFileSize } from '@fastgpt/global/common/file/tools'; -import { getFileIcon } from '@fastgpt/global/common/file/icon'; - -type FileImportFormType = { - name: string; - evaluationModel: string; - files: EvaluationFileItemType[]; - autoEvaluation: boolean; -}; - -const FileImport = () => { - const { t } = useTranslation(); - const router = useRouter(); - const { toast } = useToast(); - const { llmModelList } = useSystemStore(); - const [isFormValid, setIsFormValid] = useState(false); - const [selectFiles, setSelectFiles] = useState([]); - - const evalModelList = useMemo(() => { - return llmModelList.filter((item) => item.useInEvaluation); - }, [llmModelList]); - - const { register, setValue, watch, handleSubmit } = useForm({ - defaultValues: { - name: '', - evaluationModel: evalModelList[0]?.model || '', - files: [], - autoEvaluation: true - } - }); - - const name = watch('name'); - const evaluationModel = watch('evaluationModel'); - const files = watch('files'); - const autoEvaluation = watch('autoEvaluation'); - - const successFiles = useMemo(() => selectFiles.filter((item) => !item.errorMsg), [selectFiles]); - - // 检查表单是否有效 - const checkFormValid = useCallback(() => { - const isValid = name.trim() !== '' && successFiles.length > 0; - setIsFormValid(isValid); - }, [name, successFiles]); - - React.useEffect(() => { - checkFormValid(); - }, [checkFormValid]); - - React.useEffect(() => { - setValue('files', successFiles); - }, [setValue, successFiles]); - - const { runAsync: onSelectFiles, loading: uploading } = useRequest2( - async (files: SelectFileItemType[]) => { - await Promise.all( - files.map(async ({ fileId, file }) => { - try { - const { fileId: uploadFileId } = await uploadFile2DB({ - file, - bucketName: BucketNameEnum.dataset, - data: { - // TODO: 后续需要传入正确的数据集ID或其他必要参数 - datasetId: 'evaluation-dataset' - }, - percentListen: (e) => { - setSelectFiles((state) => - state.map((item) => - item.id === fileId - ? { - ...item, - uploadedFileRate: item.uploadedFileRate - ? Math.max(e, item.uploadedFileRate) - : e - } - : item - ) - ); - } - }); - setSelectFiles((state) => - state.map((item) => - item.id === fileId - ? { - ...item, - dbFileId: uploadFileId, - isUploading: false, - uploadedFileRate: 100 - } - : item - ) - ); - } catch (error) { - setSelectFiles((state) => - state.map((item) => - item.id === fileId - ? { - ...item, - isUploading: false, - errorMsg: getErrText(error) - } - : item - ) - ); - } - }) - ); - }, - { - onBefore([files]) { - setSelectFiles((state) => { - return [ - ...state, - ...files.map((selectFile) => { - const { fileId, file } = selectFile; - - return { - id: fileId, - createStatus: 'waiting', - file, - sourceName: file.name, - sourceSize: formatFileSize(file.size), - icon: getFileIcon(file.name), - isUploading: true, - uploadedFileRate: 0 - }; - }) - ]; - }); - } - } - ); - - const handleDownloadTemplate = () => { - const templateContent = 'question,answer\n示例问题,示例答案\n'; - fileDownload({ - text: templateContent, - type: 'text/csv;charset=utf-8', - filename: 'evaluation_template.csv' - }); - }; - - const onSubmit = async (data: FileImportFormType) => { - if (!data.name) { - return toast({ - title: t('dashboard_evaluation:file_import_name_placeholder'), - status: 'warning' - }); - } - - if (!data.files || data.files.length === 0) { - return toast({ - title: t('dashboard_evaluation:file_import_select_file'), - status: 'warning' - }); - } - - // TODO: 实现文件导入逻辑 - console.log('File import data:', data); - - toast({ - title: t('dashboard_evaluation:file_import_success'), - status: 'success' - }); - - router.push('/dashboard/evaluation?evaluationTab=datasets'); - }; - - return ( - - {() => ( - - - } - aria-label={''} - size={'smSquare'} - borderRadius={'50%'} - variant={'whiteBase'} - mr={2} - onClick={() => router.push('/dashboard/evaluation?evaluationTab=datasets')} - /> - {t('dashboard_evaluation:file_import_back')} - - - - {/* 名称输入框 */} - - - {t('dashboard_evaluation:file_import_name_label')} - - - - - {/* 文件上传 */} - - - {t('dashboard_evaluation:file_import_file_label')} - - - - {/* 渲染已选择的文件 */} - - - - - - - - - {/* 自动评测开关 */} - - - - {t('dashboard_evaluation:file_import_auto_evaluation_label')} - - - - setValue('autoEvaluation', e.target.checked)} - colorScheme="blue" - /> - - - {/* 质量评测模型 */} - - - {t('dashboard_evaluation:file_import_evaluation_model_label')} - - ({ - value: item.model, - label: item.name - }))} - onChange={(value) => setValue('evaluationModel', value)} - placeholder={t('dashboard_evaluation:file_import_evaluation_model_placeholder')} - /> - - - - - - - - - - )} - - ); -}; - -export default FileImport; - -export async function getServerSideProps(content: any) { - return { - props: { - ...(await serviceSideProps(content, ['dashboard_evaluation', 'file'])) - } - }; -} diff --git a/projects/app/src/pages/dashboard/evaluation/dataset/index.tsx b/projects/app/src/pages/dashboard/evaluation/dataset/index.tsx deleted file mode 100644 index ea63f66428d3..000000000000 --- a/projects/app/src/pages/dashboard/evaluation/dataset/index.tsx +++ /dev/null @@ -1,493 +0,0 @@ -import React, { useState, useCallback } from 'react'; -import { useRouter } from 'next/router'; -import { - Table, - Thead, - Tbody, - Tr, - Th, - Td, - TableContainer, - Box, - Flex, - HStack, - Input, - InputGroup, - InputLeftElement, - Modal, - ModalOverlay, - ModalContent, - ModalHeader, - ModalBody, - ModalCloseButton, - useDisclosure, - Text -} from '@chakra-ui/react'; -import MyBox from '@fastgpt/web/components/common/MyBox'; -import MyIconButton from '@fastgpt/web/components/common/Icon/button'; -import MyMenu from '@fastgpt/web/components/common/MyMenu'; -import MyIcon from '@fastgpt/web/components/common/Icon'; -import { useConfirm } from '@fastgpt/web/hooks/useConfirm'; -import { useRequest2 } from '@fastgpt/web/hooks/useRequest'; -import { usePagination } from '@fastgpt/web/hooks/usePagination'; -import format from 'date-fns/format'; -import UserBox from '@fastgpt/web/components/common/UserBox'; -import { useEditTitle } from '@/web/common/hooks/useEditTitle'; -import { useTranslation } from 'next-i18next'; -import MyTooltip from '@fastgpt/web/components/common/MyTooltip'; -import EmptyTip from '@fastgpt/web/components/common/EmptyTip'; -import MyTag from '@fastgpt/web/components/common/Tag/index'; -import IntelligentGeneration from '@/pageComponents/dashboard/evaluation/dataset/IntelligentGeneration'; - -// 数据集状态类型 -type DatasetStatus = - | 'queuing' - | 'parsing' - | 'generating' - | 'generateError' - | 'ready' - | 'parseError'; - -// 数据集类型 -interface EvaluationDataset { - id: number; - name: string; - dataCount: number; - status: DatasetStatus; - createTime: Date | string; - updateTime: Date | string; - creator: { - name: string; - avatar: string; - }; - errorMessage?: string; // 异常状态时的错误信息 -} - -// 模拟数据 -const mockDatasets: EvaluationDataset[] = [ - { - id: 1, - name: '数据集1', - dataCount: 100, - status: 'queuing', - createTime: '2025-05-23T10:36:13.000Z', - updateTime: '2025-05-23T10:56:13.000Z', - creator: { - name: 'violetjam', - avatar: '/imgs/avatar/BlueAvatar.svg' - } - }, - { - id: 2, - name: '数据集2', - dataCount: 100, - status: 'parsing', - createTime: '2025-05-23T10:36:13.000Z', - updateTime: '2025-05-23T10:56:13.000Z', - creator: { - name: 'violetjam', - avatar: '/imgs/avatar/BlueAvatar.svg' - } - }, - { - id: 3, - name: '数据集3', - dataCount: 100, - status: 'generating', - createTime: '2025-05-23T10:36:13.000Z', - updateTime: '2025-05-23T10:56:13.000Z', - creator: { - name: 'violetjam', - avatar: '/imgs/avatar/BlueAvatar.svg' - } - }, - { - id: 4, - name: '数据集4', - dataCount: 100, - status: 'generateError', - createTime: '2025-05-23T10:36:13.000Z', - updateTime: '2025-05-23T10:56:13.000Z', - creator: { - name: 'violetjam', - avatar: '/imgs/avatar/BlueAvatar.svg' - }, - errorMessage: '数据生成失败:模型调用异常' - }, - { - id: 5, - name: '数据集5', - dataCount: 100, - status: 'ready', - createTime: '2025-05-23T10:36:13.000Z', - updateTime: '2025-05-23T10:56:13.000Z', - creator: { - name: 'violetjam', - avatar: '/imgs/avatar/BlueAvatar.svg' - } - }, - { - id: 6, - name: '数据集6', - dataCount: 0, - status: 'parseError', - createTime: '2025-05-23T10:36:13.000Z', - updateTime: '2025-05-23T10:56:13.000Z', - creator: { - name: 'violetjam', - avatar: '/imgs/avatar/BlueAvatar.svg' - }, - errorMessage: '文件解析失败:格式不支持或文件损坏' - } -]; - -// 模拟API函数 -const getMockEvaluationDatasets = async (data: any) => { - await new Promise((resolve) => setTimeout(resolve, 300)); - - const { pageNum, pageSize, searchKey = '' } = data; - - // 过滤数据 - let filteredDatasets = mockDatasets.filter((dataset) => { - const matchesSearch = dataset.name.toLowerCase().includes(searchKey.toLowerCase()); - return matchesSearch; - }); - - // 分页 - const total = filteredDatasets.length; - const startIndex = (pageNum - 1) * pageSize; - const endIndex = startIndex + pageSize; - const list = filteredDatasets.slice(startIndex, endIndex); - - return { - list, - total - }; -}; - -const EvaluationDatasets = ({ Tab }: { Tab: React.ReactNode }) => { - const [searchValue, setSearchValue] = useState(''); - const [selectedError, setSelectedError] = useState(''); - const router = useRouter(); - const { t } = useTranslation(); - const { - isOpen: isErrorModalOpen, - onOpen: onOpenErrorModal, - onClose: onCloseErrorModal - } = useDisclosure(); - const { - isOpen: isCreateModalOpen, - onOpen: onOpenCreateModal, - onClose: onCloseCreateModal - } = useDisclosure(); - const { - isOpen: isIntelligentModalOpen, - onOpen: onOpenIntelligentModal, - onClose: onCloseIntelligentModal - } = useDisclosure(); - - // 使用分页Hook - const { - data: datasets, - Pagination, - getData: fetchData - } = usePagination(getMockEvaluationDatasets, { - defaultPageSize: 10, - params: { - searchKey: searchValue - }, - EmptyTip: , - refreshDeps: [searchValue] - }); - - // 状态配置 - const statusConfig = { - queuing: { - label: t('dashboard_evaluation:status_queuing'), - colorSchema: 'gray' - }, - parsing: { - label: t('dashboard_evaluation:status_parsing'), - colorSchema: 'blue' - }, - generating: { - label: t('dashboard_evaluation:status_generating'), - colorSchema: 'blue' - }, - generateError: { - label: t('dashboard_evaluation:status_generate_error'), - colorSchema: 'red' - }, - ready: { - label: t('dashboard_evaluation:status_ready'), - colorSchema: 'green' - }, - parseError: { - label: t('dashboard_evaluation:status_parse_error'), - colorSchema: 'red' - } - }; - - const { openConfirm, ConfirmModal } = useConfirm({ - type: 'delete' - }); - - const { onOpenModal: onOpenEditTitleModal, EditModal: EditTitleModal } = useEditTitle({ - title: t('dashboard_evaluation:rename') - }); - - // 模拟更新数据集名称的请求 - const { runAsync: onUpdateDatasetName, loading: isUpdating } = useRequest2( - (datasetId: number, newName: string) => { - console.log('updateDatasetName', datasetId, newName); - return Promise.resolve(); - }, - { - successToast: '更新成功' - } - ); - - // 渲染状态标签 - const renderStatus = (dataset: EvaluationDataset) => { - const config = statusConfig[dataset.status]; - - // 如果状态配置不存在,返回默认状态 - if (!config) { - return -; - } - - const isErrorStatus = dataset.status === 'generateError' || dataset.status === 'parseError'; - - return ( - - { - e.stopPropagation(); - setSelectedError(dataset.errorMessage || 'unknown error'); - onOpenErrorModal(); - } - : undefined - } - > - - {config.label} - {isErrorStatus && } - - - - ); - }; - - const handleDeleteDataset = (datasetId: number) => { - console.log('deleteDataset:', datasetId); - }; - - const handleRenameDataset = (dataset: EvaluationDataset) => { - onOpenEditTitleModal({ - defaultVal: dataset.name, - onSuccess: async (newName) => { - await onUpdateDatasetName(dataset.id, newName); - fetchData(); - } - }); - }; - - const handleCreateDataset = (type: 'smart' | 'import') => { - console.log('createDataset:', type); - onCloseCreateModal(); - - if (type === 'smart') { - onOpenIntelligentModal(); - } else { - // 跳转到文件导入页面 - router.push('/dashboard/evaluation/dataset/fileImport'); - } - }; - - const handleIntelligentGenerationConfirm = useCallback( - (data: any) => { - console.log('generateDataset:', data); - onCloseIntelligentModal(); - // 这里应该调用API创建数据集 - }, - [onCloseIntelligentModal] - ); - - return ( - <> - - {Tab} - - - - - - - setSearchValue(e.target.value)} - bg={'white'} - /> - - - - {t('dashboard_evaluation:create_new_dataset')} - - - } - menuList={[ - { - children: [ - { - label: ( - - - {t('dashboard_evaluation:smart_generation')} - - ), - onClick: () => handleCreateDataset('smart') - }, - { - label: ( - - - {t('dashboard_evaluation:file_import')} - - ), - onClick: () => handleCreateDataset('import') - } - ] - } - ]} - /> - - - - - - - - - - - - - - - - - - {datasets.map((dataset) => ( - - - - - - - - - ))} - -
{t('dashboard_evaluation:table_header_name')}{t('dashboard_evaluation:table_header_data_count')}{t('dashboard_evaluation:table_header_time')}{t('dashboard_evaluation:table_header_status')}{t('dashboard_evaluation:table_header_creator')}
{dataset.name}{dataset.dataCount} - {format(new Date(dataset.createTime), 'yyyy-MM-dd HH:mm:ss')} - {format(new Date(dataset.updateTime), 'yyyy-MM-dd HH:mm:ss')} - {renderStatus(dataset)} - - - handleRenameDataset(dataset) - }, - { - type: 'danger', - icon: 'delete', - label: t('dashboard_evaluation:delete'), - onClick: () => - openConfirm( - async () => { - await handleDeleteDataset(dataset.id); - fetchData(); - }, - undefined, - t('dashboard_evaluation:confirm_delete_dataset') - )() - } - ] - } - ]} - Button={} - /> -
-
-
- - - - - - {/* 异常详情弹窗 */} - - - - {t('dashboard_evaluation:error_details')} - - - {selectedError} - - - - - - - - {/* 智能生成数据集弹窗 */} - {isIntelligentModalOpen && ( - - )} - - ); -}; - -export default EvaluationDatasets; diff --git a/projects/app/src/pages/dashboard/evaluation/dimension/create.tsx b/projects/app/src/pages/dashboard/evaluation/dimension/create.tsx deleted file mode 100644 index a1594ffe6bf8..000000000000 --- a/projects/app/src/pages/dashboard/evaluation/dimension/create.tsx +++ /dev/null @@ -1,128 +0,0 @@ -import React, { useState, useCallback } from 'react'; -import MyBox from '@fastgpt/web/components/common/MyBox'; -import DashboardContainer from '@/pageComponents/dashboard/Container'; -import { useTranslation } from 'next-i18next'; -import { Button, Flex, VStack, IconButton } from '@chakra-ui/react'; -import { useRouter } from 'next/router'; -import { serviceSideProps } from '@/web/common/i18n/utils'; -import MyIcon from '@fastgpt/web/components/common/Icon'; -import { useToast } from '@fastgpt/web/hooks/useToast'; -import { useRequest2 } from '@fastgpt/web/hooks/useRequest'; -import EditForm from '@/pageComponents/dashboard/evaluation/dimension/EditForm'; -import TestRun from '@/pageComponents/dashboard/evaluation/dimension/TestRun'; -import type { EvaluationDimensionForm } from '@/pageComponents/dashboard/evaluation/dimension/EditForm'; -import { postCreateMetric } from '@/web/core/evaluation/dimension'; -import { getErrText } from '@fastgpt/global/common/error/utils'; - -const DimensionCreate = () => { - const { t } = useTranslation(); - const router = useRouter(); - const { toast } = useToast(); - const [isFormValid, setIsFormValid] = useState(false); - const [isTestRunOpen, setIsTestRunOpen] = useState(false); - - const handleValidationChange = useCallback((isValid: boolean) => { - setIsFormValid(isValid); - }, []); - - const handleTestRun = useCallback(() => { - setIsTestRunOpen(true); - }, []); - - const handleCloseTestRun = useCallback(() => { - setIsTestRunOpen(false); - }, []); - - const { runAsync: createMetric, loading: isCreating } = useRequest2( - async (data: EvaluationDimensionForm) => { - await postCreateMetric({ - name: data.name, - description: data.description, - prompt: data.prompt - }); - }, - { - onSuccess: () => { - toast({ - title: t('dashboard_evaluation:dimension_create_success'), - status: 'success' - }); - - router.push('/dashboard/evaluation?evaluationTab=dimensions'); - }, - errorToast: '', - onError: (error) => { - toast({ - title: getErrText(error), - status: 'error' - }); - } - } - ); - - const onSubmit = async (data: EvaluationDimensionForm) => { - if (!data.name) { - return toast({ - title: t('dashboard_evaluation:dimension_create_name_required'), - status: 'warning' - }); - } - - if (!data.prompt) { - return toast({ - title: t('dashboard_evaluation:dimension_create_prompt_required'), - status: 'warning' - }); - } - - await createMetric(data); - }; - - return ( - - {() => ( - - - - - - - - - - - - - {/* 试运行弹窗 */} - - - )} - - ); -}; - -export default DimensionCreate; - -export async function getServerSideProps(content: any) { - return { - props: { - ...(await serviceSideProps(content, ['dashboard_evaluation', 'file'])) - } - }; -} diff --git a/projects/app/src/pages/dashboard/evaluation/dimension/edit.tsx b/projects/app/src/pages/dashboard/evaluation/dimension/edit.tsx deleted file mode 100644 index 0d385d287ea1..000000000000 --- a/projects/app/src/pages/dashboard/evaluation/dimension/edit.tsx +++ /dev/null @@ -1,192 +0,0 @@ -import React, { useState, useCallback, useEffect } from 'react'; -import MyBox from '@fastgpt/web/components/common/MyBox'; -import DashboardContainer from '@/pageComponents/dashboard/Container'; -import { useTranslation } from 'next-i18next'; -import { Button, Flex, VStack } from '@chakra-ui/react'; -import { useRouter } from 'next/router'; -import { serviceSideProps } from '@/web/common/i18n/utils'; -import MyIcon from '@fastgpt/web/components/common/Icon'; -import { useToast } from '@fastgpt/web/hooks/useToast'; -import { useRequest2 } from '@fastgpt/web/hooks/useRequest'; -import Loading from '@fastgpt/web/components/common/MyLoading'; -import EditForm from '@/pageComponents/dashboard/evaluation/dimension/EditForm'; -import TestRun from '@/pageComponents/dashboard/evaluation/dimension/TestRun'; -import { getMetricDetail, putUpdateMetric } from '@/web/core/evaluation/dimension'; -import type { EvaluationDimensionForm } from '@/pageComponents/dashboard/evaluation/dimension/EditForm'; - -const DimensionEdit = () => { - const { t } = useTranslation(); - const router = useRouter(); - const { toast } = useToast(); - const [isFormValid, setIsFormValid] = useState(false); - const [isTestRunOpen, setIsTestRunOpen] = useState(false); - const [dimensionData, setDimensionData] = useState(null); - const [isLoading, setIsLoading] = useState(true); - - const dimensionId = router.query.id as string; - - // 获取维度数据的请求 - const { runAsync: fetchDimensionData, loading: isFetching } = useRequest2( - async (id: string) => { - const response = await getMetricDetail(id); - return response; - }, - { - manual: true, - onSuccess: (data) => { - setDimensionData({ - name: data.name, - description: data.description || '', - prompt: data.prompt || '' - }); - }, - onError: (error) => { - console.error('get dimension data error:', error); - toast({ - title: t('dashboard_evaluation:dimension_get_data_failed'), - status: 'error' - }); - router.push('/dashboard/evaluation?evaluationTab=dimensions'); - } - } - ); - - // 获取维度数据 - useEffect(() => { - const loadDimensionData = async () => { - if (!dimensionId) return; - - try { - setIsLoading(true); - await fetchDimensionData(dimensionId); - } finally { - setIsLoading(false); - } - }; - - loadDimensionData(); - }, [dimensionId, fetchDimensionData]); - - const handleValidationChange = useCallback((isValid: boolean) => { - setIsFormValid(isValid); - }, []); - - const handleTestRun = useCallback(() => { - setIsTestRunOpen(true); - }, []); - - const handleCloseTestRun = useCallback(() => { - setIsTestRunOpen(false); - }, []); - - // 更新维度的请求 - const { runAsync: updateDimension, loading: isUpdating } = useRequest2( - async (data: EvaluationDimensionForm) => { - if (!dimensionId) throw new Error('dimensionId is required'); - - await putUpdateMetric({ - id: dimensionId, - name: data.name, - description: data.description, - prompt: data.prompt - }); - }, - { - manual: true, - onSuccess: () => { - toast({ - title: t('dashboard_evaluation:dimension_update_success'), - status: 'success' - }); - router.push('/dashboard/evaluation?evaluationTab=dimensions'); - }, - onError: (error) => { - toast({ - title: t('dashboard_evaluation:dimension_update_failed'), - status: 'error' - }); - } - } - ); - - const onSubmit = async (data: EvaluationDimensionForm) => { - if (!data.name) { - return toast({ - title: t('dashboard_evaluation:dimension_name_required'), - status: 'warning' - }); - } - - await updateDimension(data); - }; - - if (isLoading || isFetching) { - return {() => }; - } - - if (!dimensionData) { - return ( - - {() => ( - - - {t('dashboard_evaluation:dimension_data_not_exist')} - - - )} - - ); - } - - return ( - - {() => ( - - - - - - - - - - - - - {/* 试运行弹窗 */} - - - )} - - ); -}; - -export default DimensionEdit; - -export async function getServerSideProps(content: any) { - return { - props: { - ...(await serviceSideProps(content, ['dashboard_evaluation', 'file'])) - } - }; -} diff --git a/projects/app/src/pages/dashboard/evaluation/dimension/index.tsx b/projects/app/src/pages/dashboard/evaluation/dimension/index.tsx deleted file mode 100644 index 336c46bd5487..000000000000 --- a/projects/app/src/pages/dashboard/evaluation/dimension/index.tsx +++ /dev/null @@ -1,214 +0,0 @@ -import React, { useState } from 'react'; -import { - Table, - Thead, - Tbody, - Tr, - Th, - Td, - TableContainer, - Box, - Flex, - Button, - HStack, - Text, - Input, - InputGroup, - InputLeftElement -} from '@chakra-ui/react'; -import MyBox from '@fastgpt/web/components/common/MyBox'; -import MyIconButton from '@fastgpt/web/components/common/Icon/button'; -import MyIcon from '@fastgpt/web/components/common/Icon'; -import MyTag from '@fastgpt/web/components/common/Tag'; -import { useConfirm } from '@fastgpt/web/hooks/useConfirm'; -import { useRequest2 } from '@fastgpt/web/hooks/useRequest'; -import { useRouter } from 'next/router'; -import { usePagination } from '@fastgpt/web/hooks/usePagination'; -import format from 'date-fns/format'; -import UserBox from '@fastgpt/web/components/common/UserBox'; -import { useTranslation } from 'next-i18next'; -import EmptyTip from '@fastgpt/web/components/common/EmptyTip'; -import { getMetricList, deleteMetric } from '@/web/core/evaluation/dimension'; -// import { EvalMetricTypeEnum } from '@fastgpt/global/core/evaluation/constants'; -// import type { EvalMetricSchemaType } from '@fastgpt/global/core/evaluation/type'; - -const EvaluationDimensions = ({ Tab }: { Tab: React.ReactNode }) => { - const [searchValue, setSearchValue] = useState(''); - const { t } = useTranslation(); - const router = useRouter(); - - // 创建适配器函数来匹配 usePagination 的参数格式 - const getMetricListAdapter = async (data: any) => { - const params = { - page: data.pageNum, - pageSize: data.pageSize, - searchKey: data.searchKey - }; - const result = await getMetricList(params); - - // 根据实际接口响应结构解析数据 - return { - list: result.list || [], - total: result.total || 0 - }; - }; - - // 使用分页Hook - const { - data: dimensions, - Pagination, - getData: fetchData - } = usePagination(getMetricListAdapter, { - defaultPageSize: 10, - params: { - searchKey: searchValue - }, - EmptyTip: , - refreshDeps: [searchValue] - }); - - const { openConfirm, ConfirmModal } = useConfirm({ - type: 'delete' - }); - - const { runAsync: onDeleteMetric } = useRequest2(deleteMetric, { - onSuccess: () => { - fetchData(); - }, - errorToast: t('dashboard_evaluation:delete_failed'), - successToast: t('dashboard_evaluation:delete_success') - }); - - const handleDeleteDimension = (dimensionId: string) => { - onDeleteMetric(dimensionId); - }; - - return ( - <> - - {Tab} - - - - - - - setSearchValue(e.target.value)} - bg={'white'} - /> - - - - - - - - - - - - - - - - - - - {dimensions.map((dimension: any) => ( - { - router.push({ - pathname: '/dashboard/evaluation/dimension/edit', - query: { id: dimension._id } - }); - }} - // onClick={ - // dimension.type === EvalMetricTypeEnum.Custom - // ? () => { - // router.push({ - // pathname: '/dashboard/evaluation/dimension/edit', - // query: { id: dimension._id } - // }); - // } - // : undefined - // } - > - - - - - - - ))} - -
{t('dashboard_evaluation:dimension_name')}{t('dashboard_evaluation:description')}{t('dashboard_evaluation:create_update_time')}{t('dashboard_evaluation:creator')}
- - {dimension.name} - {/* {dimension.type === EvalMetricTypeEnum.Builtin && ( - {t('dashboard_evaluation:builtin')} - )} */} - - {dimension.description || '-'} - {format(new Date(dimension.createTime), 'yyyy-MM-dd HH:mm:ss')} - {format(new Date(dimension.updateTime), 'yyyy-MM-dd HH:mm:ss')} - - - e.stopPropagation()}> - {/* {dimension.type === EvalMetricTypeEnum.Custom && ( */} - - openConfirm( - async () => { - await handleDeleteDimension(dimension._id); - }, - undefined, - t('dashboard_evaluation:confirm_delete_dimension') - )() - } - /> - {/* )} */} -
-
-
- - - - - - - - ); -}; - -export default EvaluationDimensions; diff --git a/projects/app/src/pages/dashboard/evaluation/index.tsx b/projects/app/src/pages/dashboard/evaluation/index.tsx deleted file mode 100644 index 1961d8544ea7..000000000000 --- a/projects/app/src/pages/dashboard/evaluation/index.tsx +++ /dev/null @@ -1,65 +0,0 @@ -'use client'; -import DashboardContainer from '../../../pageComponents/dashboard/Container'; -import { serviceSideProps } from '@/web/common/i18n/utils'; -import { useTranslation } from 'next-i18next'; -import { Flex } from '@chakra-ui/react'; -import { useState, useMemo } from 'react'; -import FillRowTabs from '@fastgpt/web/components/common/Tabs/FillRowTabs'; -import EvaluationTasks from './task/index'; -import EvaluationDatasets from './dataset/index'; -import EvaluationDimensions from './dimension/index'; -import { useRouter } from 'next/router'; - -type TabType = 'tasks' | 'datasets' | 'dimensions'; - -const Evaluation = () => { - const { t } = useTranslation(); - const router = useRouter(); - const { evaluationTab = 'tasks' } = router.query as { evaluationTab: TabType }; - - const Tab = useMemo(() => { - return ( - - list={[ - { label: t('dashboard_evaluation:evaluation_tasks_tab'), value: 'tasks' }, - { label: t('dashboard_evaluation:evaluation_datasets_tab'), value: 'datasets' }, - { label: t('dashboard_evaluation:evaluation_dimensions_tab'), value: 'dimensions' } - ]} - value={evaluationTab} - py={1} - onChange={(e) => { - router.replace({ - query: { - ...router.query, - evaluationTab: e - } - }); - }} - /> - ); - }, [router, evaluationTab, t]); - - return ( - - {({ MenuIcon }) => ( - - - {evaluationTab === 'tasks' && } - {evaluationTab === 'datasets' && } - {evaluationTab === 'dimensions' && } - - - )} - - ); -}; - -export default Evaluation; - -export async function getServerSideProps(content: any) { - return { - props: { - ...(await serviceSideProps(content, ['dashboard_evaluation'])) - } - }; -} diff --git a/projects/app/src/pages/dashboard/evaluation/task/detail/index.tsx b/projects/app/src/pages/dashboard/evaluation/task/detail/index.tsx deleted file mode 100644 index 7c686c702358..000000000000 --- a/projects/app/src/pages/dashboard/evaluation/task/detail/index.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import React from 'react'; -import { useRouter } from 'next/router'; -import { Box, Flex } from '@chakra-ui/react'; -import { useTranslation } from 'next-i18next'; -import FolderPath from '@/components/common/folder/Path'; - -const EvaluationTaskDetail = () => { - const { t } = useTranslation(); - const router = useRouter(); - - // 路径导航 - const paths = [{ parentId: 'current', parentName: 'taskName' }]; - - return ( - - {/* 顶部导航栏 */} - - {/* 路径导航 */} - - { - router.push(`/dashboard/evaluation?evaluationTab=tasks`); - }} - /> - - - - ); -}; - -export default EvaluationTaskDetail; diff --git a/projects/app/src/pages/dashboard/evaluation/task/index.tsx b/projects/app/src/pages/dashboard/evaluation/task/index.tsx deleted file mode 100644 index e13349e1e286..000000000000 --- a/projects/app/src/pages/dashboard/evaluation/task/index.tsx +++ /dev/null @@ -1,541 +0,0 @@ -import React, { useState, useMemo } from 'react'; -import { - Table, - Thead, - Tbody, - Tr, - Th, - Td, - TableContainer, - Box, - Flex, - Button, - HStack, - Input, - InputGroup, - InputLeftElement -} from '@chakra-ui/react'; -import Avatar from '@fastgpt/web/components/common/Avatar'; -import MyBox from '@fastgpt/web/components/common/MyBox'; -import MyIconButton from '@fastgpt/web/components/common/Icon/button'; -import MyMenu from '@fastgpt/web/components/common/MyMenu'; -import MyIcon from '@fastgpt/web/components/common/Icon'; -import { useConfirm } from '@fastgpt/web/hooks/useConfirm'; -import { useRequest2 } from '@fastgpt/web/hooks/useRequest'; -import { usePagination } from '@fastgpt/web/hooks/usePagination'; -import format from 'date-fns/format'; -import UserBox from '@fastgpt/web/components/common/UserBox'; -import { useEditTitle } from '@/web/common/hooks/useEditTitle'; -import { useTranslation } from 'next-i18next'; -import MyTooltip from '@fastgpt/web/components/common/MyTooltip'; -import AppSelectWithAll from '@/pageComponents/dashboard/evaluation/task/AppSelectWithAll'; -import EmptyTip from '@fastgpt/web/components/common/EmptyTip'; -import { useRouter } from 'next/router'; -import CitationTemplate from '@/pageComponents/dashboard/evaluation/dimension/CitationTemplate'; -import ConfigParamsModal from '@/pageComponents/dashboard/evaluation/task/detail/ConfigParams'; -import CreateModal from '@/pageComponents/dashboard/evaluation/task/CreateModal'; - -// 评测结果维度类型 -interface EvaluationDimension { - name: string; - score: number; -} - -// 评测结果类型 -interface EvaluationResult { - type: 'comprehensive' | 'dimensions'; // 综合评分 或 维度评分 - comprehensiveScore?: number; // 综合分数 - dimensions?: EvaluationDimension[]; // 维度分数列表 -} - -// 模拟数据类型 -interface EvaluationTask { - id: number; - name: string; - status: 'pending' | 'running' | 'completed'; - app: { - name: string; - avatar: string; - }; - version: string; - result: string; - createTime: Date | string; - finishTime?: Date | string; - executor: { - name: string; - avatar: string; - }; - // 进度相关字段 - completedCount?: number; - totalCount?: number; - // 异常数据数量 - errorCount?: number; - // 评测结果详情 - evaluationResult?: EvaluationResult; -} - -// 模拟数据 -const mockTasks: EvaluationTask[] = [ - { - id: 1, - name: '任务1', - status: 'pending', - app: { - name: '客服助手', - avatar: 'core/app/type/simpleFill' - }, - version: '2025-08-01', - result: '等待中', - createTime: '2025-08-01T00:58:08.946Z', - executor: { - name: 'violetjam', - avatar: '/imgs/avatar/BlueAvatar.svg' - } - }, - { - id: 2, - name: '任务2', - status: 'running', - app: { - name: '客服助手', - avatar: 'core/app/type/simpleFill' - }, - version: '2025-08-01', - result: '评测中', - createTime: '2025-08-01T00:58:08.946Z', - executor: { - name: 'violetjam', - avatar: '/imgs/avatar/BlueAvatar.svg' - }, - completedCount: 41, - totalCount: 50, - errorCount: 2 - }, - { - id: 3, - name: '任务3', - status: 'running', - app: { - name: '客服助手', - avatar: 'core/app/type/simpleFill' - }, - version: '2025-08-01', - result: '评测中', - createTime: '2025-08-01T00:58:08.946Z', - executor: { - name: 'violetjam', - avatar: '/imgs/avatar/BlueAvatar.svg' - }, - completedCount: 41, - totalCount: 50 - }, - { - id: 4, - name: '任务4', - status: 'completed', - app: { - name: '客服助手', - avatar: 'core/app/type/simpleFill' - }, - version: '2025-08-01', - result: '已完成', - createTime: '2025-08-01T00:58:08.946Z', - finishTime: '2025-08-01T01:58:08.946Z', - executor: { - name: 'violetjam', - avatar: '/imgs/avatar/BlueAvatar.svg' - }, - evaluationResult: { - type: 'comprehensive', - comprehensiveScore: 72 - } - }, - { - id: 5, - name: '任务5', - status: 'completed', - app: { - name: '客服助手', - avatar: 'core/app/type/simpleFill' - }, - version: '2025-08-01', - result: '已完成', - createTime: '2025-08-01T00:58:08.946Z', - finishTime: '2025-08-01T01:58:08.946Z', - executor: { - name: 'violetjam', - avatar: '/imgs/avatar/BlueAvatar.svg' - }, - evaluationResult: { - type: 'dimensions', - dimensions: [ - { name: '回答准确性', score: 62 }, - { name: '回答忠诚度', score: 78 } - ] - } - } -]; - -// 模拟API函数 - 实际项目中应该替换为真实的API调用 -const getMockEvaluationTasks = async (data: any) => { - // 模拟API延迟 - await new Promise((resolve) => setTimeout(resolve, 300)); - - const { pageNum, pageSize, searchKey = '', appFilter = '' } = data; - - // 过滤数据 - let filteredTasks = mockTasks.filter((task) => { - const matchesSearch = task.name.toLowerCase().includes(searchKey.toLowerCase()); - const matchesApp = !appFilter || task.app.name === appFilter; - return matchesSearch && matchesApp; - }); - - // 分页 - const total = filteredTasks.length; - const startIndex = (pageNum - 1) * pageSize; - const endIndex = startIndex + pageSize; - const list = filteredTasks.slice(startIndex, endIndex); - - return { - list, - total - }; -}; - -const EvaluationTasks = ({ Tab }: { Tab: React.ReactNode }) => { - const router = useRouter(); - const [searchValue, setSearchValue] = useState(''); - const [appFilter, setAppFilter] = useState(''); - const [isTemplateModalOpen, setIsTemplateModalOpen] = useState(false); - const [isConfigParamsModalOpen, setIsConfigParamsModalOpen] = useState(false); - const [isCreateModalOpen, setIsCreateModalOpen] = useState(false); - const { t } = useTranslation(); - - // 使用分页Hook - const { - data: tasks, - Pagination, - getData: fetchData - } = usePagination(getMockEvaluationTasks, { - defaultPageSize: 10, - params: { - searchKey: searchValue, - appFilter - }, - EmptyTip: , - refreshDeps: [searchValue, appFilter] - }); - - const statusMap = { - pending: { label: t('dashboard_evaluation:queuing_status'), colorSchema: undefined }, - running: { label: t('dashboard_evaluation:running_status'), colorSchema: 'blue' }, - completed: { label: t('dashboard_evaluation:completed_status'), colorSchema: 'green.600' } - }; - - const { openConfirm, ConfirmModal } = useConfirm({ - type: 'delete' - }); - - const { onOpenModal: onOpenEditTitleModal, EditModal: EditTitleModal } = useEditTitle({ - title: t('common:Rename') - }); - - // TODO: 模拟更新任务名称的请求 - const { runAsync: onUpdateTaskName, loading: isUpdating } = useRequest2( - (taskId: number, newName: string) => { - // 这里应该是实际的API调用,现在使用模拟 - console.log('更新任务名称:', taskId, newName); - return Promise.resolve(); - }, - { - successToast: t('common:update_success') - } - ); - - // 渲染评测结果 - const renderEvaluationResult = (task: EvaluationTask) => { - if (task.status === 'running') { - return {t('dashboard_evaluation:evaluating_status')}; - } - - if (task.status === 'pending') { - return {t('dashboard_evaluation:waiting')}; - } - - if (task.status === 'completed' && task.evaluationResult) { - const { evaluationResult } = task; - - if ( - evaluationResult.type === 'comprehensive' && - evaluationResult.comprehensiveScore !== undefined - ) { - // 综合评分显示 - return ( - - {evaluationResult.comprehensiveScore} - - ); - } - - if (evaluationResult.type === 'dimensions' && evaluationResult.dimensions) { - // 维度评分显示 - return ( - - {evaluationResult.dimensions.map((dimension, index) => ( - - - {dimension.score} - - - ({dimension.name}) - - - ))} - - ); - } - } - - return -; - }; - - const handleDeleteTask = (taskId: number) => { - console.log('删除任务:', taskId); - }; - - const handleRenameTask = (task: EvaluationTask) => { - onOpenEditTitleModal({ - defaultVal: task.name, - onSuccess: async (newName) => { - await onUpdateTaskName(task.id, newName); - fetchData(); // 重新获取数据 - } - }); - }; - - const handleRetryErrorData = (taskId: number) => { - console.log('重试异常数据:', taskId); - // TODO: 实现重试异常数据的API调用 - }; - - const handleCreateNewTask = () => { - setIsCreateModalOpen(true); - }; - - const handleTemplateConfirm = (template: string) => { - console.log('选择的模板:', template); - // TODO: 根据选择的模板创建新任务 - }; - - // 处理配置参数确认 - const handleConfigParamsConfirm = (config: any) => { - console.log('配置参数:', config); - // TODO: 根据配置参数创建新任务 - setIsConfigParamsModalOpen(false); - }; - - // 处理创建任务确认 - const handleCreateTaskConfirm = (data: any) => { - console.log('创建任务:', data); - // TODO: 根据表单数据创建新任务 - fetchData(); // 重新获取数据 - }; - - return ( - <> - - {Tab} - - - - - - - - - - setSearchValue(e.target.value)} - bg={'white'} - /> - - - - - - - - - - - - - - - - - - - - - - {tasks.map((task) => ( - { - router.push({ - pathname: '/dashboard/evaluation/task/detail', - query: { - taskId: task.id - } - }); - }} - > - - - - - - - - - - ))} - -
{t('dashboard_evaluation:task_name_column')}{t('dashboard_evaluation:progress_column')}{t('dashboard_evaluation:evaluation_app_column')}{t('dashboard_evaluation:app_version_column')}{t('dashboard_evaluation:evaluation_result_column')}{t('dashboard_evaluation:start_finish_time_column')}{t('dashboard_evaluation:executor_column')}
{task.name} - {task.status === 'running' && - task.completedCount !== undefined && - task.totalCount !== undefined ? ( - - - - {task.completedCount} - - - /{task.totalCount} - - - {task.errorCount && task.errorCount > 0 && ( - - - - )} - - ) : ( - - {statusMap[task.status]?.label} - - )} - - - - {task.app.name} - - {task.version}{renderEvaluationResult(task)} - {format(new Date(task.createTime), 'yyyy-MM-dd HH:mm:ss')} - - {task.finishTime - ? format(new Date(task.finishTime), 'yyyy-MM-dd HH:mm:ss') - : '-'} - - - - e.stopPropagation()}> - 0 - ? [ - { - icon: 'common/retryLight', - label: t('dashboard_evaluation:retry_error_data'), - onClick: () => handleRetryErrorData(task.id) - } - ] - : []), - { - icon: 'edit', - label: t('dashboard_evaluation:rename'), - onClick: () => handleRenameTask(task) - }, - { - type: 'danger', - icon: 'delete', - label: t('dashboard_evaluation:delete'), - onClick: () => - openConfirm( - async () => { - await handleDeleteTask(task.id); - fetchData(); // 删除后重新获取数据 - }, - undefined, - t('dashboard_evaluation:confirm_delete_task') - )() - } - ] - } - ]} - Button={} - /> -
-
-
- - - - - - - - setIsTemplateModalOpen(false)} - onConfirm={handleTemplateConfirm} - /> - setIsConfigParamsModalOpen(false)} - onConfirm={handleConfigParamsConfirm} - /> - {isCreateModalOpen && ( - setIsCreateModalOpen(false)} - onSubmit={handleCreateTaskConfirm} - /> - )} - - ); -}; - -export default EvaluationTasks; diff --git a/test/cases/function/packages/service/support/permission/evaluation/.env b/test/cases/function/packages/service/support/permission/evaluation/.env new file mode 100644 index 000000000000..b2a91a00c28b --- /dev/null +++ b/test/cases/function/packages/service/support/permission/evaluation/.env @@ -0,0 +1,31 @@ +# 评估权限功能测试环境配置示例 +# 复制此文件为 .env 并填入实际值 + +# FastGPT服务器地址 +FASTGPT_BASE_URL=http://localhost:3000 + +# 测试用户令牌配置 +# 获取方式: 在FastGPT前端登录后,从浏览器开发者工具中获取Authorization header + +# 拥有管理权限的用户token (必需) +OWNER_TOKEN=fastgpt-d0uH3v3f6snsdO4SOkSCmWats4bGvblKWHV2OqkmnaB6XDNqLUkabo7DxER41u1 +# 普通成员用户token (可选,用于更全面的权限测试) +MEMBER_TOKEN=your-member-token-here + +# 只读权限用户token (可选) +READONLY_TOKEN=your-readonly-token-here + +# 无权限用户token (可选,用于权限拒绝测试) +NO_ACCESS_TOKEN=your-no-access-token-here + +# 简化测试使用的token (可以使用OWNER_TOKEN的值) +TEST_TOKEN=fastgpt-d0uH3v3f6snsdO4SOkSCmWats4bGvblKWHV2OqkmnaB6XDNqLUkabo7DxER41u1 + +# 测试用的应用ID (可选,用于评估任务创建测试) +TEST_APP_ID=your-test-app-id-here + +# 用户ID配置 (可选,高级测试使用) +OWNER_USER_ID=your-owner-user-id +MEMBER_USER_ID=your-member-user-id +READONLY_USER_ID=your-readonly-user-id +NO_ACCESS_USER_ID=your-no-access-user-id \ No newline at end of file diff --git a/test/cases/function/packages/service/support/permission/evaluation/.env.example b/test/cases/function/packages/service/support/permission/evaluation/.env.example new file mode 100644 index 000000000000..4f03ead82171 --- /dev/null +++ b/test/cases/function/packages/service/support/permission/evaluation/.env.example @@ -0,0 +1,32 @@ +# 评估权限功能测试环境配置示例 +# 复制此文件为 .env 并填入实际值 + +# FastGPT服务器地址 +FASTGPT_BASE_URL=http://localhost:3000 + +# 测试用户令牌配置 +# 获取方式: 在FastGPT前端登录后,从浏览器开发者工具中获取Authorization header + +# 拥有管理权限的用户token (必需) +OWNER_TOKEN=your-owner-token-here + +# 普通成员用户token (可选,用于更全面的权限测试) +MEMBER_TOKEN=your-member-token-here + +# 只读权限用户token (可选) +READONLY_TOKEN=your-readonly-token-here + +# 无权限用户token (可选,用于权限拒绝测试) +NO_ACCESS_TOKEN=your-no-access-token-here + +# 简化测试使用的token (可以使用OWNER_TOKEN的值) +TEST_TOKEN=your-test-token-here + +# 测试用的应用ID (可选,用于评估任务创建测试) +TEST_APP_ID=your-test-app-id-here + +# 用户ID配置 (可选,高级测试使用) +OWNER_USER_ID=your-owner-user-id +MEMBER_USER_ID=your-member-user-id +READONLY_USER_ID=your-readonly-user-id +NO_ACCESS_USER_ID=your-no-access-user-id \ No newline at end of file diff --git a/test/cases/function/packages/service/support/permission/evaluation/README.md b/test/cases/function/packages/service/support/permission/evaluation/README.md new file mode 100644 index 000000000000..b1ede18e3965 --- /dev/null +++ b/test/cases/function/packages/service/support/permission/evaluation/README.md @@ -0,0 +1,205 @@ +# FastGPT 评估权限功能测试 + +这个目录包含了FastGPT评估模块权限控制功能的端到端功能测试。 + +## 测试概述 + +功能测试通过真实的HTTP请求验证评估权限系统的正确性,包括: + +- ✅ **权限认证**: 验证用户身份和权限获取 +- ✅ **权限过滤**: 验证只返回用户有权限访问的资源 +- ✅ **CRUD权限**: 验证创建、读取、更新、删除操作的权限控制 +- ✅ **权限层次**: 验证read < write < manage的权限层次关系 +- ✅ **边界测试**: 验证无效请求和异常情况的处理 + +## 文件说明 + +### 核心测试文件 +- `demo.js` - 基础功能演示脚本 +- `evaluation-permissions.test.ts` - 完整功能测试套件 +- `evaluation-permissions-simple.test.ts` - 简化功能测试套件 +- `run-evaluation-tests.sh` - 测试运行脚本 + +### 配置和文档 +- `.env.example` - 环境变量配置示例 +- `get-token-guide.md` - Token获取指南 +- `README.md` - 本说明文档 + +## 测试文件 + +### 1. 基础功能测试 +**文件**: `evaluation-permissions-simple.test.ts` + +这是一个轻量级的测试,适合快速验证基本功能: +- 验证API可访问性 +- 验证权限信息返回 +- 验证无认证请求拒绝 +- 验证响应时间合理性 + +### 2. 完整功能测试 +**文件**: `evaluation-permissions.test.ts` + +这是一个全面的测试套件,包含: +- 多用户权限测试 +- 资源生命周期测试(创建→读取→更新→删除) +- 权限边界和异常处理测试 +- 性能测试 + +## 快速开始 + +### 1. 配置环境 + +复制环境配置模板: +```bash +cp test/cases/function/packages/service/support/permission/evaluation/.env.example test/cases/function/packages/service/support/permission/evaluation/.env +``` + +编辑 `.env` 文件,填入必需的配置: +```env +# FastGPT服务地址 +FASTGPT_BASE_URL=http://localhost:3000 + +# 基础测试令牌(必需) +TEST_TOKEN=your-auth-token-here +``` + +### 2. 获取测试令牌 + +1. 在FastGPT前端登录 +2. 打开浏览器开发者工具 (F12) +3. 转到Network标签页,刷新页面 +4. 找到任意API请求,复制Authorization header的值 +5. 将令牌填入 `.env` 文件的 `TEST_TOKEN` 字段 + +### 3. 运行测试 + +#### 方法一:demo演示脚本 +```bash +node test/cases/function/packages/service/support/permission/evaluation/demo.js +``` + +#### 方法二:使用自动化脚本 +```bash +chmod +x test/cases/function/packages/service/support/permission/evaluation/run-evaluation-tests.sh +./test/cases/function/packages/service/support/permission/evaluation/run-evaluation-tests.sh +``` + +#### 方法三:直接运行测试 +```bash +# 基础功能测试 +pnpm test test/cases/function/packages/service/support/permission/evaluation/evaluation-permissions-simple.test.ts + +# 完整功能测试 +pnpm test test/cases/function/packages/service/support/permission/evaluation/evaluation-permissions.test.ts +``` + +## 环境配置详解 + +### 必需配置 +- `FASTGPT_BASE_URL`: FastGPT服务的基础URL +- `TEST_TOKEN`: 有效的用户认证令牌 + +### 可选配置(用于高级测试) +- `OWNER_TOKEN`: 拥有管理权限的用户令牌 +- `MEMBER_TOKEN`: 普通成员用户令牌 +- `READONLY_TOKEN`: 只读权限用户令牌 +- `NO_ACCESS_TOKEN`: 无权限用户令牌 +- `TEST_APP_ID`: 测试用的应用ID + +## 测试场景 + +### 基础权限验证 +- ✅ 验证已认证用户可以访问资源列表 +- ✅ 验证未认证用户被正确拒绝 +- ✅ 验证返回的资源包含正确的权限信息 +- ✅ 验证权限层次关系(manage > write > read) + +### 资源级权限 +- ✅ **评估任务**: 验证任务的创建、查看、修改、删除权限 +- ✅ **评估数据集**: 验证数据集的权限控制 +- ✅ **评估指标**: 验证指标的权限控制 + +### 权限过滤 +- ✅ 验证列表API只返回用户有读权限的资源 +- ✅ 验证详情API正确返回权限信息 +- ✅ 验证无权限资源被正确过滤 + +### 异常情况 +- ✅ 无效令牌处理 +- ✅ 过期令牌处理 +- ✅ 不存在的资源ID处理 +- ✅ 网络超时处理 + +## 性能测试 + +功能测试还包含性能验证: +- 权限检查不应显著影响API响应时间(< 5秒) +- 大量数据的权限过滤应该高效(< 3秒) +- 并发权限验证应该稳定 + +## 故障排查 + +### 测试失败常见原因 + +1. **连接失败** + - 检查`FASTGPT_BASE_URL`配置是否正确 + - 确认FastGPT服务正在运行 + - 检查网络连接 + +2. **认证失败 (401/403)** + - 检查`TEST_TOKEN`是否有效 + - 令牌可能已过期,需要重新获取 + - 确认用户有足够的权限 + +3. **权限验证失败** + - 检查用户是否对测试资源有相应权限 + - 确认权限系统配置正确 + +4. **超时错误** + - 检查服务响应性能 + - 可以适当增加超时时间配置 + +### 调试技巧 + +1. **启用详细日志**:测试会输出详细的操作信息 +2. **逐步测试**:先运行基础测试,再运行完整测试 +3. **检查API响应**:查看控制台输出的错误详情 + +## 扩展测试 + +如需添加新的测试场景: + +1. **添加新API端点测试**:在`EvaluationAPIClient`中添加新方法 +2. **添加新权限场景**:在测试用例中添加新的权限验证逻辑 +3. **添加性能测试**:在性能测试部分添加新的基准测试 + +## 最佳实践 + +1. **定期运行**:在权限相关代码变更后运行功能测试 +2. **CI/CD集成**:将测试集成到持续集成流水线 +3. **多环境测试**:在开发、测试、预生产环境都运行测试 +4. **监控性能**:关注权限验证对API性能的影响 + +## 测试报告 + +测试完成后会显示: +- ✅ 通过的测试数量 +- ❌ 失败的测试详情 +- ⏱️ 性能指标 +- 📊 权限验证覆盖率 + +示例输出: +``` +🎉 评估权限功能测试完成! +✅ 基础功能测试: 通过 (5/5) +✅ 完整功能测试: 通过 (15/15) +⏱️ 平均响应时间: 245ms +📊 权限验证覆盖率: 100% +``` + +## 支持 + +如遇到问题,请: +1. 查看故障排查部分 +2. 检查控制台错误输出 +3. 提交issue到FastGPT仓库 \ No newline at end of file diff --git a/test/cases/function/packages/service/support/permission/evaluation/demo.js b/test/cases/function/packages/service/support/permission/evaluation/demo.js new file mode 100755 index 000000000000..ff831fbc10c3 --- /dev/null +++ b/test/cases/function/packages/service/support/permission/evaluation/demo.js @@ -0,0 +1,267 @@ +#!/usr/bin/env node + +/** + * FastGPT 评估权限智能测试脚本 + * + * 自动检测token类型并使用正确的认证方式 + * 运行方式: node test/cases/function/packages/service/support/permission/evaluation/smart-demo.js + */ + +const fs = require('fs'); +const path = require('path'); + +// 加载.env文件 +function loadEnvFile() { + const envPath = path.join(__dirname, '.env'); + + if (fs.existsSync(envPath)) { + console.log('📂 正在加载环境配置文件...'); + const envContent = fs.readFileSync(envPath, 'utf8'); + + envContent.split('\n').forEach((line) => { + line = line.trim(); + if (line && !line.startsWith('#')) { + const [key, value] = line.split('=', 2); + if (key && value) { + process.env[key.trim()] = value.trim(); + } + } + }); + + console.log('✅ 环境配置已加载\n'); + } +} + +// 检测token类型 +function detectTokenType(token) { + if (!token) return 'none'; + + if (token.startsWith('fastgpt-')) { + return 'apikey'; + } else if (token.startsWith('eyJ')) { + return 'jwt'; + } else if (token.length > 50) { + return 'session'; + } else { + return 'unknown'; + } +} + +// 创建认证headers +function createAuthHeaders(token, tokenType) { + const headers = { + 'Content-Type': 'application/json' + }; + + switch (tokenType) { + case 'apikey': + headers['Authorization'] = `Bearer ${token}`; + break; + case 'jwt': + case 'session': + headers['token'] = token; + break; + case 'cookie': + headers['Cookie'] = `token=${token}`; + break; + } + + return headers; +} + +// 测试API调用 +async function testApiCall(url, token, tokenType, testName) { + const headers = createAuthHeaders(token, tokenType); + + try { + const response = await fetch(url, { + method: 'POST', + headers, + body: JSON.stringify({ pageNum: 1, pageSize: 5 }), + signal: AbortSignal.timeout(10000) + }); + + if (response.ok) { + const data = await response.json(); + const itemCount = data.data?.list?.length || 0; + const totalCount = data.data?.total || 0; + console.log(`✅ ${testName}: 成功 (${itemCount}/${totalCount} 项可访问)`); + console.log(data); + if (data.data?.list && data.data.list.length > 0 && data.data.list[0].permission) { + console.log( + ` 权限信息: 读${data.data.list[0].permission.hasReadPer} 写${data.data.list[0].permission.hasWritePer} 管理${data.data.list[0].permission.hasManagePer}` + ); + } + return { success: true, data }; + } else { + const errorText = await response.text(); + console.log(`❌ ${testName}: HTTP ${response.status}`); + console.log(` 错误详情: ${errorText}`); + return { success: false, status: response.status, error: errorText }; + } + } catch (error) { + console.log(`❌ ${testName}: ${error.message}`); + return { success: false, error: error.message }; + } +} + +// 主测试函数 +async function smartTest() { + console.log('🚀 FastGPT 评估权限智能测试\n'); + + loadEnvFile(); + + const API_BASE = process.env.FASTGPT_BASE_URL || 'http://localhost:3000'; + const TEST_TOKEN = process.env.TEST_TOKEN || ''; + + if (!TEST_TOKEN) { + console.log('❌ 错误: 未配置TEST_TOKEN环境变量'); + console.log( + '💡 请查看 test/cases/function/packages/service/support/permission/evaluation/get-token-guide.md 了解如何获取token\n' + ); + process.exit(1); + } + + const tokenType = detectTokenType(TEST_TOKEN); + + console.log('🔧 测试配置:'); + console.log(` 服务地址: ${API_BASE}`); + console.log(` Token长度: ${TEST_TOKEN.length} 字符`); + console.log(` Token类型: ${tokenType}`); + console.log(` Token预览: ${TEST_TOKEN.substring(0, 20)}...`); + console.log(); + + if (tokenType === 'unknown') { + console.log('⚠️ 警告: 无法识别的token格式'); + console.log( + '💡 建议查看 test/cases/function/packages/service/support/permission/evaluation/get-token-guide.md 获取正确的token\n' + ); + } + + // 测试不同的认证方式 + console.log('🔍 开始权限测试...\n'); + + const apiEndpoints = [ + { name: '评估任务', path: '/api/core/evaluation/task/list' }, + { name: '评估数据集', path: '/api/core/evaluation/dataset/collection/list' }, + { name: '评估指标', path: '/api/core/evaluation/metric/list' } + ]; + + let successCount = 0; + const results = []; + + // 首先尝试检测到的token类型 + console.log(`📋 使用${tokenType}认证方式测试...\n`); + + for (const endpoint of apiEndpoints) { + const result = await testApiCall( + `${API_BASE}${endpoint.path}`, + TEST_TOKEN, + tokenType, + endpoint.name + ); + + results.push({ ...endpoint, result, authType: tokenType }); + if (result.success) successCount++; + } + + // 如果主要认证方式失败,尝试其他方式 + if (successCount === 0) { + console.log('\n🔄 尝试其他认证方式...\n'); + + const alternativeTypes = ['jwt', 'session', 'apikey', 'cookie'].filter((t) => t !== tokenType); + + for (const altType of alternativeTypes) { + console.log(`📋 尝试${altType}认证方式...\n`); + + let altSuccessCount = 0; + for (const endpoint of apiEndpoints) { + const result = await testApiCall( + `${API_BASE}${endpoint.path}`, + TEST_TOKEN, + altType, + endpoint.name + ); + + if (result.success) { + altSuccessCount++; + console.log(`✨ 发现可用的认证方式: ${altType}\n`); + break; + } + } + + if (altSuccessCount > 0) { + successCount = altSuccessCount; + break; + } + } + } + + // 测试无认证请求 + console.log('\n🔍 测试无认证请求拒绝...'); + try { + const response = await fetch(`${API_BASE}/api/core/evaluation/task/list`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ pageNum: 1, pageSize: 5 }), + signal: AbortSignal.timeout(5000) + }); + + if (response.status === 401 || response.status === 403) { + console.log('✅ 无认证请求正确被拒绝\n'); + } else { + console.log(`⚠️ 意外响应: HTTP ${response.status}\n`); + } + } catch (error) { + console.log(`⚠️ 无认证测试出错: ${error.message}\n`); + } + + // 总结报告 + console.log('📊 测试结果汇总:'); + console.log('================'); + console.log(`成功的API调用: ${successCount}/${apiEndpoints.length}`); + console.log(`检测到的认证方式: ${tokenType}`); + + if (successCount > 0) { + console.log('\n🎉 权限系统工作正常!'); + console.log('\n💡 后续步骤:'); + console.log( + '1. 运行完整测试: ./test/cases/function/packages/service/support/permission/evaluation/run-evaluation-tests.sh' + ); + console.log( + '2. 或运行vitest: pnpm test test/cases/function/packages/service/support/permission/evaluation/evaluation-permissions-simple.test.ts' + ); + } else { + console.log('\n💥 所有测试失败!'); + console.log('\n🔧 故障排查步骤:'); + console.log('1. 检查FastGPT服务是否运行: curl http://localhost:3000'); + console.log( + '2. 重新获取token: 查看 test/cases/function/packages/service/support/permission/evaluation/get-token-guide.md' + ); + console.log('3. 验证用户权限: 确保用户有评估模块访问权限'); + console.log('4. 检查token有效性: 尝试在浏览器中手动访问FastGPT'); + + // 提供具体的调试命令 + console.log('\n🛠️ 调试命令:'); + if (tokenType === 'apikey') { + console.log(`curl -X POST "${API_BASE}/api/core/evaluation/task/list" \\`); + console.log(` -H "Content-Type: application/json" \\`); + console.log(` -H "Authorization: Bearer ${TEST_TOKEN}" \\`); + console.log(` -d '{"pageNum": 1, "pageSize": 5}'`); + } else { + console.log(`curl -X POST "${API_BASE}/api/core/evaluation/task/list" \\`); + console.log(` -H "Content-Type: application/json" \\`); + console.log(` -H "token: ${TEST_TOKEN}" \\`); + console.log(` -d '{"pageNum": 1, "pageSize": 5}'`); + } + } + + console.log(''); +} + +// 运行测试 +smartTest().catch((error) => { + console.error('\n💥 测试过程中出现错误:'); + console.error(error); + process.exit(1); +}); diff --git a/test/cases/function/packages/service/support/permission/evaluation/evaluation-permissions-simple.test.ts b/test/cases/function/packages/service/support/permission/evaluation/evaluation-permissions-simple.test.ts new file mode 100644 index 000000000000..93fafb1f6fc1 --- /dev/null +++ b/test/cases/function/packages/service/support/permission/evaluation/evaluation-permissions-simple.test.ts @@ -0,0 +1,269 @@ +/** + * 评估权限简化功能测试 + * + * 这是一个简化版的功能测试,用于快速验证评估权限功能 + * 运行方式: pnpm test test/cases/function/packages/service/support/permission/evaluation/evaluation-permissions-simple.test.ts + */ + +import { describe, expect, it } from 'vitest'; + +// 简单的HTTP请求实现,避免依赖axios +const httpRequest = async (url: string, options: any = {}) => { + const response = await fetch(url, { + method: options.method || 'GET', + headers: { + 'Content-Type': 'application/json', + ...options.headers + }, + body: options.body ? JSON.stringify(options.body) : undefined, + signal: AbortSignal.timeout(options.timeout || 10000) + }); + + if (!response.ok) { + const error = new Error(`HTTP ${response.status}: ${response.statusText}`); + (error as any).response = { status: response.status, data: await response.text() }; + throw error; + } + + return { + status: response.status, + data: await response.json() + }; +}; + +// 简化的测试配置 +const API_BASE = process.env.FASTGPT_BASE_URL || 'http://localhost:3000'; +const TEST_TOKEN = process.env.TEST_TOKEN || ''; + +// 创建HTTP客户端 +const createClient = (token?: string) => { + return { + post: async (path: string, data?: any) => { + return httpRequest(`${API_BASE}${path}`, { + method: 'POST', + headers: { + ...(token && { Authorization: `Bearer ${token}` }) + }, + body: data, + timeout: 10000 + }); + } + }; +}; + +describe('评估权限基础功能测试', () => { + const client = createClient(TEST_TOKEN); + + it('应该能够获取评估任务列表', async () => { + if (!TEST_TOKEN) { + console.warn('跳过测试: 未配置TEST_TOKEN环境变量'); + return; + } + + try { + const response = await client.post('/api/core/evaluation/task/list', { + pageNum: 1, + pageSize: 10 + }); + + expect(response.status).toBe(200); + expect(response.data).toHaveProperty('list'); + expect(response.data).toHaveProperty('total'); + expect(Array.isArray(response.data.list)).toBe(true); + + // 验证每个任务都有权限属性 + response.data.list.forEach((task: any) => { + expect(task).toHaveProperty('permission'); + expect(task.permission).toHaveProperty('hasReadPer'); + expect(task.permission).toHaveProperty('hasWritePer'); + expect(task.permission).toHaveProperty('hasManagePer'); + }); + + console.log(`✅ 获取到 ${response.data.list.length} 个评估任务`); + } catch (error: any) { + if (error.response?.status === 401 || error.response?.status === 403) { + console.warn('⚠️ 认证失败,请检查TEST_TOKEN配置'); + } else { + console.error('❌ API调用失败:', error.message); + throw error; + } + } + }); + + it('应该能够获取评估数据集列表', async () => { + if (!TEST_TOKEN) { + console.warn('跳过测试: 未配置TEST_TOKEN环境变量'); + return; + } + + try { + const response = await client.post('/api/core/evaluation/dataset/list', { + pageNum: 1, + pageSize: 10 + }); + + expect(response.status).toBe(200); + expect(response.data).toHaveProperty('list'); + expect(response.data).toHaveProperty('total'); + + console.log(`✅ 获取到 ${response.data.list.length} 个评估数据集`); + } catch (error: any) { + if (error.response?.status === 401 || error.response?.status === 403) { + console.warn('⚠️ 认证失败,请检查TEST_TOKEN配置'); + } else { + throw error; + } + } + }); + + it('应该能够获取评估指标列表', async () => { + if (!TEST_TOKEN) { + console.warn('跳过测试: 未配置TEST_TOKEN环境变量'); + return; + } + + try { + const response = await client.post('/api/core/evaluation/metric/list', { + pageNum: 1, + pageSize: 10 + }); + + expect(response.status).toBe(200); + expect(response.data).toHaveProperty('list'); + expect(response.data).toHaveProperty('total'); + + console.log(`✅ 获取到 ${response.data.list.length} 个评估指标`); + } catch (error: any) { + if (error.response?.status === 401 || error.response?.status === 403) { + console.warn('⚠️ 认证失败,请检查TEST_TOKEN配置'); + } else { + throw error; + } + } + }); + + it('无认证请求应该被拒绝', async () => { + const unauthenticatedClient = createClient(); + + try { + await unauthenticatedClient.post('/api/core/evaluation/task/list', { + pageNum: 1, + pageSize: 10 + }); + + throw new Error('预期请求应该被拒绝,但实际成功了'); + } catch (error: any) { + expect(error.response?.status).toBeOneOf([401, 403]); + console.log('✅ 无认证请求正确被拒绝'); + } + }); + + it('权限验证响应时间应该合理', async () => { + if (!TEST_TOKEN) { + console.warn('跳过测试: 未配置TEST_TOKEN环境变量'); + return; + } + + const start = Date.now(); + + try { + await Promise.all([ + client.post('/api/core/evaluation/task/list', { pageNum: 1, pageSize: 5 }), + client.post('/api/core/evaluation/dataset/list', { pageNum: 1, pageSize: 5 }), + client.post('/api/core/evaluation/metric/list', { pageNum: 1, pageSize: 5 }) + ]); + + const duration = Date.now() - start; + expect(duration).toBeLessThan(5000); // 应该在5秒内完成 + + console.log(`✅ 权限验证响应时间: ${duration}ms`); + } catch (error: any) { + if (error.response?.status !== 401 && error.response?.status !== 403) { + throw error; + } + } + }); +}); + +describe('评估权限数据完整性测试', () => { + const client = createClient(TEST_TOKEN); + + it('返回的资源应该包含完整的权限信息', async () => { + if (!TEST_TOKEN) { + console.warn('跳过测试: 未配置TEST_TOKEN环境变量'); + return; + } + + try { + const response = await client.post('/api/core/evaluation/task/list', { + pageNum: 1, + pageSize: 5 + }); + + if (response.data.list.length === 0) { + console.warn('⚠️ 没有可测试的数据,跳过权限信息验证'); + return; + } + + const task = response.data.list[0]; + + // 验证权限对象结构 + expect(task.permission).toBeDefined(); + expect(typeof task.permission.hasReadPer).toBe('boolean'); + expect(typeof task.permission.hasWritePer).toBe('boolean'); + expect(typeof task.permission.hasManagePer).toBe('boolean'); + + // 验证权限层次关系 + if (task.permission.hasManagePer) { + expect(task.permission.hasWritePer).toBe(true); + expect(task.permission.hasReadPer).toBe(true); + } + + if (task.permission.hasWritePer) { + expect(task.permission.hasReadPer).toBe(true); + } + + console.log(`✅ 权限信息验证通过:`, { + read: task.permission.hasReadPer, + write: task.permission.hasWritePer, + manage: task.permission.hasManagePer + }); + } catch (error: any) { + if (error.response?.status !== 401 && error.response?.status !== 403) { + throw error; + } + } + }); + + it('应该正确过滤无权限的资源', async () => { + if (!TEST_TOKEN) { + console.warn('跳过测试: 未配置TEST_TOKEN环境变量'); + return; + } + + try { + const [tasks, datasets, metrics] = await Promise.all([ + client.post('/api/core/evaluation/task/list', { pageNum: 1, pageSize: 100 }), + client.post('/api/core/evaluation/dataset/list', { pageNum: 1, pageSize: 100 }), + client.post('/api/core/evaluation/metric/list', { pageNum: 1, pageSize: 100 }) + ]); + + // 所有返回的资源都应该至少有读权限 + [...tasks.data.list, ...datasets.data.list, ...metrics.data.list].forEach((resource: any) => { + if (resource.permission) { + expect(resource.permission.hasReadPer).toBe(true); + } + }); + + console.log('✅ 资源权限过滤验证通过:', { + tasks: tasks.data.list.length, + datasets: datasets.data.list.length, + metrics: metrics.data.list.length + }); + } catch (error: any) { + if (error.response?.status !== 401 && error.response?.status !== 403) { + throw error; + } + } + }); +}); diff --git a/test/cases/function/packages/service/support/permission/evaluation/evaluation-permissions.test.ts b/test/cases/function/packages/service/support/permission/evaluation/evaluation-permissions.test.ts new file mode 100644 index 000000000000..f549ce8d1a72 --- /dev/null +++ b/test/cases/function/packages/service/support/permission/evaluation/evaluation-permissions.test.ts @@ -0,0 +1,570 @@ +/** + * 评估权限功能测试 + * + * 此测试脚本用于验证FastGPT评估模块的权限控制功能 + * 通过实际的HTTP请求测试API接口的权限验证 + */ + +import { describe, expect, it, beforeAll, afterAll } from 'vitest'; + +// HTTP请求类型定义 +interface HttpResponse { + status: number; + data: T; +} + +// 简单的HTTP请求实现 +const httpRequest = async (url: string, options: any = {}): Promise => { + const response = await fetch(url, { + method: options.method || 'GET', + headers: { + 'Content-Type': 'application/json', + ...options.headers + }, + body: options.body ? JSON.stringify(options.body) : undefined, + signal: AbortSignal.timeout(options.timeout || 30000) + }); + + const data = response.ok ? await response.json() : await response.text(); + + if (!response.ok) { + const error = new Error(`HTTP ${response.status}: ${response.statusText}`); + (error as any).response = { status: response.status, data }; + throw error; + } + + return { + status: response.status, + data + }; +}; + +// 测试环境配置 +const TEST_CONFIG = { + baseURL: process.env.FASTGPT_BASE_URL || 'http://localhost:3000', + timeout: 30000, + // 测试用户配置 + users: { + owner: { + token: process.env.OWNER_TOKEN || '', + userId: process.env.OWNER_USER_ID || '' + }, + member: { + token: process.env.MEMBER_TOKEN || '', + userId: process.env.MEMBER_USER_ID || '' + }, + readOnly: { + token: process.env.READONLY_TOKEN || '', + userId: process.env.READONLY_USER_ID || '' + }, + noAccess: { + token: process.env.NO_ACCESS_TOKEN || '', + userId: process.env.NO_ACCESS_USER_ID || '' + } + } +}; + +// API客户端类 +class EvaluationAPIClient { + private baseURL: string; + private token?: string; + + constructor(token?: string) { + this.baseURL = TEST_CONFIG.baseURL; + this.token = token; + } + + private async request(path: string, data?: any): Promise { + return httpRequest(`${this.baseURL}${path}`, { + method: 'POST', + headers: { + ...(this.token && { Authorization: `Bearer ${this.token}` }) + }, + body: data, + timeout: TEST_CONFIG.timeout + }); + } + + // 评估任务相关API + async createEvaluationTask(data: any): Promise { + return this.request('/api/core/evaluation/task/create', data); + } + + async listEvaluationTasks(params: any = {}): Promise { + return this.request('/api/core/evaluation/task/list', params); + } + + async getEvaluationTaskDetail(taskId: string): Promise { + return this.request('/api/core/evaluation/task/detail', { evaluationId: taskId }); + } + + async updateEvaluationTask(taskId: string, data: any): Promise { + return this.request('/api/core/evaluation/task/update', { evaluationId: taskId, ...data }); + } + + async deleteEvaluationTask(taskId: string): Promise { + return this.request('/api/core/evaluation/task/delete', { evaluationId: taskId }); + } + + // 评估数据集相关API + async createEvaluationDataset(data: any): Promise { + return this.request('/api/core/evaluation/dataset/create', data); + } + + async listEvaluationDatasets(params: any = {}): Promise { + return this.request('/api/core/evaluation/dataset/list', params); + } + + async getEvaluationDatasetDetail(datasetId: string): Promise { + return this.request('/api/core/evaluation/dataset/detail', { datasetId }); + } + + async updateEvaluationDataset(datasetId: string, data: any): Promise { + return this.request('/api/core/evaluation/dataset/update', { datasetId, ...data }); + } + + async deleteEvaluationDataset(datasetId: string): Promise { + return this.request('/api/core/evaluation/dataset/delete', { datasetId }); + } + + // 评估指标相关API + async createEvaluationMetric(data: any): Promise { + return this.request('/api/core/evaluation/metric/create', data); + } + + async listEvaluationMetrics(params: any = {}): Promise { + return this.request('/api/core/evaluation/metric/list', params); + } + + async getEvaluationMetricDetail(metricId: string): Promise { + return this.request('/api/core/evaluation/metric/detail', { metricId }); + } + + async updateEvaluationMetric(metricId: string, data: any): Promise { + return this.request('/api/core/evaluation/metric/update', { metricId, ...data }); + } + + async deleteEvaluationMetric(metricId: string): Promise { + return this.request('/api/core/evaluation/metric/delete', { metricId }); + } +} + +// 测试数据生成器 +class TestDataGenerator { + static generateDataset() { + return { + name: `测试数据集_${Date.now()}`, + description: '功能测试自动生成的数据集', + dataFormat: 'csv', + columns: [ + { name: 'userInput', type: 'string', required: true, description: '用户输入' }, + { name: 'expectedOutput', type: 'string', required: true, description: '期望输出' } + ], + dataItems: [ + { userInput: '你好', expectedOutput: '您好,有什么可以帮助您的吗?' }, + { userInput: '今天天气怎么样?', expectedOutput: '很抱歉,我无法获取实时天气信息。' } + ] + }; + } + + static generateMetric() { + return { + name: `测试指标_${Date.now()}`, + description: '功能测试自动生成的指标', + type: 'ai_model', + config: { + model: 'gpt-3.5-turbo', + systemPrompt: '请评估回答的质量,给出1-5的分数', + temperature: 0.1 + } + }; + } + + static generateTask(datasetId: string, metricIds: string[]) { + return { + name: `测试任务_${Date.now()}`, + description: '功能测试自动生成的评估任务', + datasetId, + target: { + type: 'workflow', + config: { + appId: process.env.TEST_APP_ID || 'test-app-id' + } + }, + metricIds + }; + } +} + +// 权限验证助手 +class PermissionTestHelper { + static async expectPermissionDenied(operation: () => Promise) { + try { + await operation(); + throw new Error('Expected permission denied, but operation succeeded'); + } catch (error: any) { + expect(error.response?.status).toBeOneOf([401, 403]); + } + } + + static async expectSuccess(operation: () => Promise): Promise { + try { + const response = await operation(); + expect(response.status).toBe(200); + return response; + } catch (error: any) { + console.error('Operation failed:', error.response?.data || error.message); + throw error; + } + } + + static expectPermissionProperty( + data: any, + expectedPermissions: { + hasReadPer: boolean; + hasWritePer: boolean; + hasManagePer: boolean; + } + ) { + expect(data.permission).toBeDefined(); + expect(data.permission.hasReadPer).toBe(expectedPermissions.hasReadPer); + expect(data.permission.hasWritePer).toBe(expectedPermissions.hasWritePer); + expect(data.permission.hasManagePer).toBe(expectedPermissions.hasManagePer); + } +} + +describe('评估权限功能测试', () => { + let ownerClient: EvaluationAPIClient; + let memberClient: EvaluationAPIClient; + let readOnlyClient: EvaluationAPIClient; + let noAccessClient: EvaluationAPIClient; + + // 测试资源ID + let testDatasetId: string; + let testMetricId: string; + let testTaskId: string; + + beforeAll(() => { + // 验证测试环境配置 + if (!TEST_CONFIG.users.owner.token) { + console.warn('警告: 未配置OWNER_TOKEN环境变量,部分测试可能跳过'); + } + + // 初始化API客户端 + ownerClient = new EvaluationAPIClient(TEST_CONFIG.users.owner.token); + memberClient = new EvaluationAPIClient(TEST_CONFIG.users.member.token); + readOnlyClient = new EvaluationAPIClient(TEST_CONFIG.users.readOnly.token); + noAccessClient = new EvaluationAPIClient(TEST_CONFIG.users.noAccess.token); + }); + + afterAll(async () => { + // 清理测试数据 + try { + if (testTaskId) { + await ownerClient.deleteEvaluationTask(testTaskId); + } + if (testDatasetId) { + await ownerClient.deleteEvaluationDataset(testDatasetId); + } + if (testMetricId) { + await ownerClient.deleteEvaluationMetric(testMetricId); + } + } catch (error) { + console.warn('清理测试数据时出错:', error); + } + }); + + describe('评估数据集权限测试', () => { + it('Owner用户应该能够创建数据集', async () => { + if (!TEST_CONFIG.users.owner.token) { + console.warn('跳过测试: 未配置OWNER_TOKEN'); + return; + } + + const datasetData = TestDataGenerator.generateDataset(); + const response = await PermissionTestHelper.expectSuccess(() => + ownerClient.createEvaluationDataset(datasetData) + ); + + testDatasetId = response.data.datasetId; + expect(testDatasetId).toBeDefined(); + }); + + it('Owner用户应该能够查看数据集列表', async () => { + if (!TEST_CONFIG.users.owner.token) { + console.warn('跳过测试: 未配置OWNER_TOKEN'); + return; + } + + const response = await PermissionTestHelper.expectSuccess(() => + ownerClient.listEvaluationDatasets({ pageNum: 1, pageSize: 10 }) + ); + + expect(response.data.list).toBeInstanceOf(Array); + if (response.data.list.length > 0) { + PermissionTestHelper.expectPermissionProperty(response.data.list[0], { + hasReadPer: true, + hasWritePer: true, + hasManagePer: true + }); + } + }); + + it('无权限用户不应该能够创建数据集', async () => { + if (!TEST_CONFIG.users.noAccess.token) { + console.warn('跳过测试: 未配置NO_ACCESS_TOKEN'); + return; + } + + const datasetData = TestDataGenerator.generateDataset(); + await PermissionTestHelper.expectPermissionDenied(() => + noAccessClient.createEvaluationDataset(datasetData) + ); + }); + + it('只读用户应该能够查看但不能修改数据集', async () => { + if (!TEST_CONFIG.users.readOnly.token || !testDatasetId) { + console.warn('跳过测试: 未配置READONLY_TOKEN或无测试数据集'); + return; + } + + // 应该能够查看详情 + const detailResponse = await PermissionTestHelper.expectSuccess(() => + readOnlyClient.getEvaluationDatasetDetail(testDatasetId) + ); + + PermissionTestHelper.expectPermissionProperty(detailResponse.data, { + hasReadPer: true, + hasWritePer: false, + hasManagePer: false + }); + + // 不应该能够更新 + await PermissionTestHelper.expectPermissionDenied(() => + readOnlyClient.updateEvaluationDataset(testDatasetId, { name: '尝试修改' }) + ); + }); + }); + + describe('评估指标权限测试', () => { + it('Owner用户应该能够创建指标', async () => { + if (!TEST_CONFIG.users.owner.token) { + console.warn('跳过测试: 未配置OWNER_TOKEN'); + return; + } + + const metricData = TestDataGenerator.generateMetric(); + const response = await PermissionTestHelper.expectSuccess(() => + ownerClient.createEvaluationMetric(metricData) + ); + + testMetricId = response.data.metricId; + expect(testMetricId).toBeDefined(); + }); + + it('Member用户应该能够查看指标列表', async () => { + if (!TEST_CONFIG.users.member.token) { + console.warn('跳过测试: 未配置MEMBER_TOKEN'); + return; + } + + const response = await PermissionTestHelper.expectSuccess(() => + memberClient.listEvaluationMetrics({ pageNum: 1, pageSize: 10 }) + ); + + expect(response.data.list).toBeInstanceOf(Array); + }); + + it('无权限用户不应该能够删除指标', async () => { + if (!TEST_CONFIG.users.noAccess.token || !testMetricId) { + console.warn('跳过测试: 未配置NO_ACCESS_TOKEN或无测试指标'); + return; + } + + await PermissionTestHelper.expectPermissionDenied(() => + noAccessClient.deleteEvaluationMetric(testMetricId) + ); + }); + }); + + describe('评估任务权限测试', () => { + it('Owner用户应该能够创建任务', async () => { + if (!TEST_CONFIG.users.owner.token || !testDatasetId || !testMetricId) { + console.warn('跳过测试: 缺少必要的配置或依赖资源'); + return; + } + + const taskData = TestDataGenerator.generateTask(testDatasetId, [testMetricId]); + const response = await PermissionTestHelper.expectSuccess(() => + ownerClient.createEvaluationTask(taskData) + ); + + testTaskId = response.data.evaluationId; + expect(testTaskId).toBeDefined(); + }); + + it('Owner用户应该能够管理任务', async () => { + if (!TEST_CONFIG.users.owner.token || !testTaskId) { + console.warn('跳过测试: 未配置OWNER_TOKEN或无测试任务'); + return; + } + + // 查看详情 + const detailResponse = await PermissionTestHelper.expectSuccess(() => + ownerClient.getEvaluationTaskDetail(testTaskId) + ); + + PermissionTestHelper.expectPermissionProperty(detailResponse.data, { + hasReadPer: true, + hasWritePer: true, + hasManagePer: true + }); + + // 更新任务 + await PermissionTestHelper.expectSuccess(() => + ownerClient.updateEvaluationTask(testTaskId, { name: '更新后的任务名称' }) + ); + }); + + it('成员用户应该根据权限级别访问任务', async () => { + if (!TEST_CONFIG.users.member.token || !testTaskId) { + console.warn('跳过测试: 未配置MEMBER_TOKEN或无测试任务'); + return; + } + + // 查看任务列表 + const listResponse = await PermissionTestHelper.expectSuccess(() => + memberClient.listEvaluationTasks({ pageNum: 1, pageSize: 10 }) + ); + + expect(listResponse.data.list).toBeInstanceOf(Array); + + // 如果有权限访问任务,验证权限属性 + const task = listResponse.data.list.find((item: any) => item._id === testTaskId); + if (task) { + expect(task.permission).toBeDefined(); + expect(task.permission.hasReadPer).toBe(true); + } + }); + }); + + describe('权限边界测试', () => { + it('应该正确处理无效的资源ID', async () => { + if (!TEST_CONFIG.users.owner.token) { + console.warn('跳过测试: 未配置OWNER_TOKEN'); + return; + } + + const invalidId = '000000000000000000000000'; + + // 尝试访问不存在的资源 + try { + await ownerClient.getEvaluationTaskDetail(invalidId); + throw new Error('Expected error for invalid resource ID'); + } catch (error: any) { + expect(error.response?.status).toBeOneOf([400, 404]); + } + }); + + it('应该正确处理未认证的请求', async () => { + const unauthenticatedClient = new EvaluationAPIClient(); + + await PermissionTestHelper.expectPermissionDenied(() => + unauthenticatedClient.listEvaluationTasks() + ); + }); + + it('应该正确处理过期的Token', async () => { + const expiredTokenClient = new EvaluationAPIClient('expired-token'); + + await PermissionTestHelper.expectPermissionDenied(() => + expiredTokenClient.listEvaluationTasks() + ); + }); + }); + + describe('权限继承和聚合测试', () => { + it('应该正确聚合团队和个人权限', async () => { + if (!TEST_CONFIG.users.member.token) { + console.warn('跳过测试: 未配置MEMBER_TOKEN'); + return; + } + + // 查看成员的资源列表,验证权限聚合 + const response = await PermissionTestHelper.expectSuccess(() => + memberClient.listEvaluationTasks({ pageNum: 1, pageSize: 10 }) + ); + + // 每个返回的资源都应该至少有读权限 + response.data.list.forEach((task: any) => { + expect(task.permission.hasReadPer).toBe(true); + }); + }); + + it('应该正确过滤无权限的资源', async () => { + if (!TEST_CONFIG.users.readOnly.token) { + console.warn('跳过测试: 未配置READONLY_TOKEN'); + return; + } + + // 只读用户应该只能看到有读权限的资源 + const response = await PermissionTestHelper.expectSuccess(() => + readOnlyClient.listEvaluationTasks({ pageNum: 1, pageSize: 10 }) + ); + + // 所有返回的任务都应该有读权限 + response.data.list.forEach((task: any) => { + expect(task.permission.hasReadPer).toBe(true); + }); + }); + }); +}); + +// 性能测试 +describe('评估权限性能测试', () => { + let ownerClient: EvaluationAPIClient; + + beforeAll(() => { + ownerClient = new EvaluationAPIClient(TEST_CONFIG.users.owner.token); + }); + + it('权限检查不应该显著影响API响应时间', async () => { + if (!TEST_CONFIG.users.owner.token) { + console.warn('跳过测试: 未配置OWNER_TOKEN'); + return; + } + + const start = Date.now(); + + // 并发请求多个资源列表 + const promises = [ + ownerClient.listEvaluationTasks({ pageSize: 50 }), + ownerClient.listEvaluationDatasets({ pageSize: 50 }), + ownerClient.listEvaluationMetrics({ pageSize: 50 }) + ]; + + await Promise.all(promises); + + const duration = Date.now() - start; + + // 权限检查不应该让API响应时间超过5秒 + expect(duration).toBeLessThan(5000); + }); + + it('大量数据下的权限过滤性能', async () => { + if (!TEST_CONFIG.users.owner.token) { + console.warn('跳过测试: 未配置OWNER_TOKEN'); + return; + } + + const start = Date.now(); + + // 请求大量数据进行权限过滤 + await ownerClient.listEvaluationTasks({ pageSize: 100 }); + + const duration = Date.now() - start; + + // 即使有大量数据,权限过滤也不应该超过3秒 + expect(duration).toBeLessThan(3000); + }); +}); diff --git a/test/cases/function/packages/service/support/permission/evaluation/get-token-guide.md b/test/cases/function/packages/service/support/permission/evaluation/get-token-guide.md new file mode 100644 index 000000000000..a046776bb58c --- /dev/null +++ b/test/cases/function/packages/service/support/permission/evaluation/get-token-guide.md @@ -0,0 +1,119 @@ +# FastGPT Token 获取指南 + +## 方法1: 获取Session Token(推荐用于测试) + +### 步骤: +1. **登录FastGPT前端** + - 打开浏览器访问 http://localhost:3000 + - 登录你的FastGPT账号 + +2. **打开开发者工具** + - 按 F12 或右键 -> 检查元素 + - 切换到 "Application" 或 "存储" 标签页 + +3. **查找Cookie中的Token** + - 在左侧面板找到 "Cookies" -> "http://localhost:3000" + - 查找名为 `token` 的cookie值 + - 复制这个值 + +4. **或者从请求Header获取** + - 切换到 "Network" 标签页 + - 刷新页面或进行任何操作 + - 找到任意一个API请求 + - 查看Request Headers,找到 `token: xxx` 这一行 + - 复制token值 + +### 更新.env文件: +```bash +TEST_TOKEN=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiI... +``` + +## 方法2: 获取API Key + +### 步骤: +1. **登录FastGPT前端** +2. **进入应用管理** +3. **打开任意应用的API访问页面** +4. **复制API Key** + - 格式通常是:`fastgpt-xxxxxxxxx` + +### 更新.env文件: +```bash +TEST_TOKEN=fastgpt-pJvXySapvRI8iGk6liymuOecLG0GlGGhC5eVWTw78OrpUdPazovNdy +``` + +## 验证Token有效性 + +### 使用curl测试: + +**Session Token测试:** +```bash +curl -X POST http://localhost:3000/api/core/evaluation/task/list \ + -H "Content-Type: application/json" \ + -H "token: YOUR_SESSION_TOKEN_HERE" \ + -d '{"pageNum": 1, "pageSize": 5}' +``` + +**API Key测试:** +```bash +curl -X POST http://localhost:3000/api/core/evaluation/task/list \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer YOUR_API_KEY_HERE" \ + -d '{"pageNum": 1, "pageSize": 5}' +``` + +## 故障排查 + +### 常见问题: + +1. **403权限错误** + - Token格式不正确 + - Token已过期 + - 用户没有评估模块权限 + +2. **Token过期** + - Session Token通常24小时过期 + - 需要重新登录获取新token + +3. **权限不足** + - 确认用户对评估模块有访问权限 + - 确认用户在正确的团队中 + +### 检查Token格式: + +**Session Token特征:** +- 长度较长(通常100+字符) +- 以 `eyJ` 开头(JWT格式) +- 包含3个用点分隔的部分 + +**API Key特征:** +- 以 `fastgpt-` 开头 +- 长度固定 +- 字母数字组合 + +## 当前你的Token分析 + +根据你的.env文件,当前token是: +``` +fastgpt-pJvXySapvRI8iGk6liymuOecLG0GlGGhC5eVWTw78OrpUdPazovNdy +``` + +这是一个API Key格式的token。如果仍然出现403错误,可能原因: + +1. **API Key无效或过期** +2. **API Key对应的应用没有评估权限** +3. **需要指定appId** + +### 解决建议: + +1. **重新获取Session Token**: + - 按照方法1获取session token + - 更新TEST_TOKEN为session token + +2. **或者获取新的API Key**: + - 确保API Key对应的应用有评估模块权限 + - 检查API Key是否正确复制 + +3. **检查FastGPT配置**: + - 确保评估模块已启用 + - 确保用户有相应权限 \ No newline at end of file diff --git a/test/cases/function/packages/service/support/permission/evaluation/run-evaluation-tests.sh b/test/cases/function/packages/service/support/permission/evaluation/run-evaluation-tests.sh new file mode 100755 index 000000000000..51ab0e448a2b --- /dev/null +++ b/test/cases/function/packages/service/support/permission/evaluation/run-evaluation-tests.sh @@ -0,0 +1,122 @@ +#!/bin/bash + +# 评估权限功能测试运行脚本 +# 使用方法: ./test/cases/function/packages/service/support/permission/evaluation/run-evaluation-tests.sh + +set -e + +echo "🚀 开始运行评估权限功能测试..." + +# 检查环境配置 +ENV_FILE="test/cases/function/packages/service/support/permission/evaluation/.env" +ENV_EXAMPLE="test/cases/function/packages/service/support/permission/evaluation/.env.example" + +if [ ! -f "$ENV_FILE" ]; then + echo "⚠️ 未找到环境配置文件,正在创建..." + if [ -f "$ENV_EXAMPLE" ]; then + cp "$ENV_EXAMPLE" "$ENV_FILE" + echo "📝 已创建 $ENV_FILE,请编辑此文件并填入正确的配置值" + echo "💡 配置说明:" + echo " - FASTGPT_BASE_URL: FastGPT服务地址" + echo " - TEST_TOKEN: 测试用户令牌 (从浏览器开发者工具获取)" + echo "" + echo "⏸️ 请配置环境变量后重新运行此脚本" + exit 1 + else + echo "❌ 未找到环境配置示例文件" + exit 1 + fi +fi + +# 加载环境变量 +set -a +source "$ENV_FILE" +set +a + +# 检查必需的环境变量 +if [ -z "$TEST_TOKEN" ]; then + echo "❌ 必须配置 TEST_TOKEN 环境变量" + echo "💡 提示: 可以在浏览器开发者工具中获取 Authorization header 的值" + exit 1 +fi + +if [ -z "$FASTGPT_BASE_URL" ]; then + echo "⚠️ 未配置 FASTGPT_BASE_URL,使用默认值: http://localhost:3000" + export FASTGPT_BASE_URL="http://localhost:3000" +fi + +echo "🔧 测试配置:" +echo " 服务地址: $FASTGPT_BASE_URL" +echo " 认证令牌: ${TEST_TOKEN:0:20}..." + +# 检查FastGPT服务是否可用 +echo "" +echo "🔍 检查FastGPT服务可用性..." +if curl -s --max-time 10 "$FASTGPT_BASE_URL/api/health" > /dev/null 2>&1; then + echo "✅ FastGPT服务可访问" +else + echo "⚠️ 无法访问FastGPT服务,测试可能会失败" + echo " 请确保服务正在运行并且地址配置正确" +fi + +echo "" +echo "🧪 运行基础功能测试..." + +# 运行简化测试 +if pnpm test test/cases/function/packages/service/support/permission/evaluation/evaluation-permissions-simple.test.ts; then + echo "✅ 基础功能测试通过" + TEST_BASIC_PASSED=1 +else + echo "❌ 基础功能测试失败" + TEST_BASIC_PASSED=0 +fi + +echo "" +echo "🧪 运行完整功能测试..." + +# 运行完整测试(如果配置了更多环境变量) +if [ -n "$OWNER_TOKEN" ] && [ -n "$MEMBER_TOKEN" ]; then + echo "📊 检测到多用户配置,运行完整权限测试..." + if pnpm test test/cases/function/packages/service/support/permission/evaluation/evaluation-permissions.test.ts; then + echo "✅ 完整功能测试通过" + TEST_FULL_PASSED=1 + else + echo "❌ 完整功能测试失败" + TEST_FULL_PASSED=0 + fi +else + echo "⚠️ 未配置多用户环境变量,跳过完整测试" + echo "💡 提示: 配置 OWNER_TOKEN、MEMBER_TOKEN 等可运行更全面的权限测试" + TEST_FULL_PASSED=-1 +fi + +echo "" +echo "📊 测试结果汇总:" +echo "==================" + +if [ $TEST_BASIC_PASSED -eq 1 ]; then + echo "✅ 基础功能测试: 通过" +else + echo "❌ 基础功能测试: 失败" +fi + +if [ $TEST_FULL_PASSED -eq 1 ]; then + echo "✅ 完整功能测试: 通过" +elif [ $TEST_FULL_PASSED -eq 0 ]; then + echo "❌ 完整功能测试: 失败" +else + echo "⏸️ 完整功能测试: 跳过" +fi + +echo "" +if [ $TEST_BASIC_PASSED -eq 1 ]; then + echo "🎉 评估权限功能测试完成!权限系统工作正常。" + exit 0 +else + echo "💥 测试失败!请检查:" + echo " 1. FastGPT服务是否正常运行" + echo " 2. 环境配置是否正确" + echo " 3. 用户令牌是否有效" + echo " 4. 网络连接是否正常" + exit 1 +fi \ No newline at end of file diff --git a/test/cases/pages/api/core/evaluation/dataset/collection/create.test.ts b/test/cases/pages/api/core/evaluation/dataset/collection/create.test.ts index 559140f820bf..84768005e41e 100644 --- a/test/cases/pages/api/core/evaluation/dataset/collection/create.test.ts +++ b/test/cases/pages/api/core/evaluation/dataset/collection/create.test.ts @@ -1,23 +1,23 @@ import { describe, expect, it, vi, beforeEach } from 'vitest'; import { handler_test } from '@/pages/api/core/evaluation/dataset/collection/create'; -import { authUserPer } from '@fastgpt/service/support/permission/user/auth'; +import { authEvaluationDatasetCreate } from '@fastgpt/service/core/evaluation/common'; import { mongoSessionRun } from '@fastgpt/service/common/mongo/sessionRun'; import { MongoEvalDatasetCollection } from '@fastgpt/service/core/evaluation/dataset/evalDatasetCollectionSchema'; -import { WritePermissionVal } from '@fastgpt/global/support/permission/constant'; -vi.mock('@fastgpt/service/support/permission/user/auth'); +vi.mock('@fastgpt/service/core/evaluation/common'); vi.mock('@fastgpt/service/common/mongo/sessionRun'); vi.mock('@fastgpt/service/core/evaluation/dataset/evalDatasetCollectionSchema', () => ({ MongoEvalDatasetCollection: { findOne: vi.fn(), create: vi.fn() - } + }, + EvalDatasetCollectionName: 'eval_dataset_collections' })); vi.mock('@fastgpt/service/support/user/audit/util', () => ({ addAuditLog: vi.fn() })); -const mockAuthUserPer = vi.mocked(authUserPer); +const mockAuthEvaluationDatasetCreate = vi.mocked(authEvaluationDatasetCreate); const mockMongoSessionRun = vi.mocked(mongoSessionRun); const mockMongoEvalDatasetCollection = vi.mocked(MongoEvalDatasetCollection); @@ -29,7 +29,7 @@ describe('EvalDatasetCollection Create API', () => { beforeEach(() => { vi.clearAllMocks(); - mockAuthUserPer.mockResolvedValue({ + mockAuthEvaluationDatasetCreate.mockResolvedValue({ teamId: validTeamId, tmbId: validTmbId }); @@ -134,30 +134,29 @@ describe('EvalDatasetCollection Create API', () => { }); describe('Authentication and Authorization', () => { - it('should call authUserPer with correct parameters', async () => { + it('should call authEvaluationDatasetCreate with correct parameters', async () => { const req = { body: { name: 'Test Dataset', description: 'Test description' } }; await handler_test(req as any); - expect(mockAuthUserPer).toHaveBeenCalledWith({ + expect(mockAuthEvaluationDatasetCreate).toHaveBeenCalledWith({ req, authToken: true, - authApiKey: true, - per: WritePermissionVal + authApiKey: true }); }); it('should propagate authentication errors', async () => { const authError = new Error('Authentication failed'); - mockAuthUserPer.mockRejectedValue(authError); + mockAuthEvaluationDatasetCreate.mockRejectedValue(authError); const req = { body: { name: 'Test Dataset', description: 'Test description' } }; - await expect(handler_test(req as any)).rejects.toBe(authError); + await expect(handler_test(req as any)).rejects.toThrow('Authentication failed'); }); }); diff --git a/test/cases/pages/api/core/evaluation/dataset/collection/delete.test.ts b/test/cases/pages/api/core/evaluation/dataset/collection/delete.test.ts index 75c7b70e09fb..070da274980f 100644 --- a/test/cases/pages/api/core/evaluation/dataset/collection/delete.test.ts +++ b/test/cases/pages/api/core/evaluation/dataset/collection/delete.test.ts @@ -1,22 +1,22 @@ import { describe, expect, it, vi, beforeEach } from 'vitest'; import { handler_test } from '@/pages/api/core/evaluation/dataset/collection/delete'; -import { authUserPer } from '@fastgpt/service/support/permission/user/auth'; +import { authEvaluationDatasetWrite } from '@fastgpt/service/core/evaluation/common'; import { mongoSessionRun } from '@fastgpt/service/common/mongo/sessionRun'; import { MongoEvalDatasetCollection } from '@fastgpt/service/core/evaluation/dataset/evalDatasetCollectionSchema'; import { MongoEvalDatasetData } from '@fastgpt/service/core/evaluation/dataset/evalDatasetDataSchema'; -import { WritePermissionVal } from '@fastgpt/global/support/permission/constant'; import { removeEvalDatasetSmartGenerateJobsRobust } from '@fastgpt/service/core/evaluation/dataset/smartGenerateMq'; import { removeEvalDatasetDataQualityJobsRobust } from '@fastgpt/service/core/evaluation/dataset/dataQualityMq'; import { removeEvalDatasetDataSynthesizeJobsRobust } from '@fastgpt/service/core/evaluation/dataset/dataSynthesizeMq'; import { addLog } from '@fastgpt/service/common/system/log'; -vi.mock('@fastgpt/service/support/permission/user/auth'); +vi.mock('@fastgpt/service/core/evaluation/common'); vi.mock('@fastgpt/service/common/mongo/sessionRun'); vi.mock('@fastgpt/service/core/evaluation/dataset/evalDatasetCollectionSchema', () => ({ MongoEvalDatasetCollection: { findOne: vi.fn(), deleteOne: vi.fn() - } + }, + EvalDatasetCollectionName: 'eval_dataset_collections' })); vi.mock('@fastgpt/service/core/evaluation/dataset/evalDatasetDataSchema', () => ({ MongoEvalDatasetData: { @@ -43,7 +43,7 @@ vi.mock('@fastgpt/service/support/user/audit/util', () => ({ addAuditLog: vi.fn() })); -const mockAuthUserPer = vi.mocked(authUserPer); +const mockAuthEvaluationDatasetWrite = vi.mocked(authEvaluationDatasetWrite); const mockMongoSessionRun = vi.mocked(mongoSessionRun); const mockMongoEvalDatasetCollection = vi.mocked(MongoEvalDatasetCollection); const mockMongoEvalDatasetData = vi.mocked(MongoEvalDatasetData); @@ -79,9 +79,10 @@ describe('EvalDatasetCollection Delete API', () => { beforeEach(() => { vi.clearAllMocks(); - mockAuthUserPer.mockResolvedValue({ + mockAuthEvaluationDatasetWrite.mockResolvedValue({ teamId: validTeamId, - tmbId: validTmbId + tmbId: validTmbId, + datasetId: validCollectionId }); mockMongoSessionRun.mockImplementation(async (callback) => { @@ -175,24 +176,23 @@ describe('EvalDatasetCollection Delete API', () => { }); describe('Authentication and Authorization', () => { - it('should call authUserPer with correct parameters', async () => { + it('should call authEvaluationDatasetWrite with correct parameters', async () => { const req = { query: { collectionId: validCollectionId } }; await handler_test(req as any); - expect(mockAuthUserPer).toHaveBeenCalledWith({ + expect(mockAuthEvaluationDatasetWrite).toHaveBeenCalledWith(validCollectionId, { req, authToken: true, - authApiKey: true, - per: WritePermissionVal + authApiKey: true }); }); it('should propagate authentication errors', async () => { const authError = new Error('Authentication failed'); - mockAuthUserPer.mockRejectedValue(authError); + mockAuthEvaluationDatasetWrite.mockRejectedValue(authError); const req = { query: { collectionId: validCollectionId } @@ -639,7 +639,7 @@ describe('EvalDatasetCollection Delete API', () => { const result = await handler_test(req as any); // Verify complete flow - expect(mockAuthUserPer).toHaveBeenCalled(); + expect(mockAuthEvaluationDatasetWrite).toHaveBeenCalled(); expect(mockMongoEvalDatasetCollection.findOne).toHaveBeenCalled(); expect(mockRemoveEvalDatasetSmartGenerateJobsRobust).toHaveBeenCalled(); expect(mockRemoveEvalDatasetDataQualityJobsRobust).toHaveBeenCalled(); diff --git a/test/cases/pages/api/core/evaluation/dataset/collection/list.test.ts b/test/cases/pages/api/core/evaluation/dataset/collection/list.test.ts index 75c917d7e871..5b314884da43 100644 --- a/test/cases/pages/api/core/evaluation/dataset/collection/list.test.ts +++ b/test/cases/pages/api/core/evaluation/dataset/collection/list.test.ts @@ -1,16 +1,17 @@ import { describe, expect, it, vi, beforeEach } from 'vitest'; import { handler_test } from '@/pages/api/core/evaluation/dataset/collection/list'; -import { authUserPer } from '@fastgpt/service/support/permission/user/auth'; +import { getEvaluationPermissionAggregation } from '@fastgpt/service/core/evaluation/common'; import { MongoEvalDatasetCollection } from '@fastgpt/service/core/evaluation/dataset/evalDatasetCollectionSchema'; import { ReadPermissionVal } from '@fastgpt/global/support/permission/constant'; import { Types } from '@fastgpt/service/common/mongo'; -vi.mock('@fastgpt/service/support/permission/user/auth'); +vi.mock('@fastgpt/service/core/evaluation/common'); vi.mock('@fastgpt/service/core/evaluation/dataset/evalDatasetCollectionSchema', () => ({ MongoEvalDatasetCollection: { aggregate: vi.fn(), countDocuments: vi.fn() - } + }, + EvalDatasetCollectionName: 'eval_dataset_collections' })); vi.mock('@fastgpt/service/core/evaluation/dataset/dataSynthesizeMq', () => ({ evalDatasetDataSynthesizeQueue: { @@ -24,7 +25,67 @@ vi.mock('@fastgpt/service/core/evaluation/dataset/dataSynthesizeMq', () => ({ } })); -const mockAuthUserPer = vi.mocked(authUserPer); +vi.mock('@fastgpt/service/support/permission/schema', () => ({ + MongoResourcePermission: { + find: vi.fn().mockReturnValue({ + lean: vi.fn().mockResolvedValue([]) + }) + } +})); + +vi.mock('@fastgpt/service/support/permission/memberGroup/controllers', () => ({ + getGroupsByTmbId: vi.fn().mockResolvedValue([]) +})); + +vi.mock('@fastgpt/service/support/permission/org/controllers', () => ({ + getOrgIdSetWithParentByTmbId: vi.fn().mockResolvedValue(new Set()) +})); + +vi.mock('@fastgpt/service/support/user/utils', () => ({ + addSourceMember: vi.fn().mockImplementation(({ list }) => Promise.resolve(list)) +})); + +vi.mock('@fastgpt/global/support/permission/evaluation/controller', () => ({ + EvaluationPermission: vi.fn().mockImplementation(() => ({ + hasReadPer: true, + hasWritePer: true, + hasManagePer: true + })) +})); + +// Mock mongoose to use the existing Types from @fastgpt/service/common/mongo +vi.mock('mongoose', async () => { + const { Types } = await vi.importActual( + '@fastgpt/service/common/mongo' + ); + return { Types }; +}); + +// Mock TeamPermission实例 +const createMockTeamPermission = (isOwner = true) => ({ + isOwner, + hasManagePer: true, + hasWritePer: true, + hasReadPer: true, + hasManageRole: true, + hasWriteRole: true, + hasReadRole: true, + hasAppCreateRole: true, + hasDatasetCreateRole: true, + hasApikeyCreateRole: true, + hasEvaluationCreateRole: true, + hasAppCreatePer: true, + hasDatasetCreatePer: true, + hasApikeyCreatePer: true, + hasEvaluationCreatePer: true, + role: 511, + permission: 511, + roleList: [], + perList: [], + rolePerMap: {} +}); + +const mockGetEvaluationPermissionAggregation = vi.mocked(getEvaluationPermissionAggregation); const mockMongoEvalDatasetCollection = vi.mocked(MongoEvalDatasetCollection); describe('EvalDatasetCollection List API', () => { @@ -35,32 +96,34 @@ describe('EvalDatasetCollection List API', () => { _id: '65f5b5b5b5b5b5b5b5b5b5b1', name: 'Dataset 1', description: 'First dataset', + tmbId: validTmbId, createTime: new Date('2024-01-01'), updateTime: new Date('2024-01-02'), - teamMember: { - avatar: 'avatar1.jpg', - name: 'User One' - } + creatorAvatar: 'avatar1.jpg', + creatorName: 'User One' }, { _id: '65f5b5b5b5b5b5b5b5b5b5b2', name: 'Dataset 2', description: 'Second dataset', + tmbId: validTmbId, createTime: new Date('2024-01-03'), updateTime: new Date('2024-01-04'), - teamMember: { - avatar: 'avatar2.jpg', - name: 'User Two' - } + creatorAvatar: 'avatar2.jpg', + creatorName: 'User Two' } ]; beforeEach(() => { vi.clearAllMocks(); - mockAuthUserPer.mockResolvedValue({ + mockGetEvaluationPermissionAggregation.mockResolvedValue({ teamId: validTeamId, - tmbId: validTmbId + tmbId: validTmbId, + isOwner: true, + roleList: [], + myGroupMap: new Map(), + myOrgSet: new Set() }); mockMongoEvalDatasetCollection.aggregate.mockResolvedValue(mockCollections); @@ -68,24 +131,23 @@ describe('EvalDatasetCollection List API', () => { }); describe('Authentication and Authorization', () => { - it('should call authUserPer with correct parameters', async () => { + it('should call getEvaluationPermissionAggregation with correct parameters', async () => { const req = { body: { pageNum: 1, pageSize: 10 } }; await handler_test(req as any); - expect(mockAuthUserPer).toHaveBeenCalledWith({ + expect(mockGetEvaluationPermissionAggregation).toHaveBeenCalledWith({ req, authToken: true, - authApiKey: true, - per: ReadPermissionVal + authApiKey: true }); }); it('should propagate authentication errors', async () => { const authError = new Error('Authentication failed'); - mockAuthUserPer.mockRejectedValue(authError); + mockGetEvaluationPermissionAggregation.mockRejectedValue(authError); const req = { body: { pageNum: 1, pageSize: 10 } @@ -106,9 +168,7 @@ describe('EvalDatasetCollection List API', () => { expect(mockMongoEvalDatasetCollection.aggregate).toHaveBeenCalledWith( expect.arrayContaining([ { $match: { teamId: new Types.ObjectId(validTeamId) } }, - { $sort: { createTime: -1 } }, - { $skip: 0 }, - { $limit: 20 } + { $sort: { createTime: -1 } } ]) ); expect(result.total).toBe(2); @@ -140,7 +200,12 @@ describe('EvalDatasetCollection List API', () => { await handler_test(req as any); expect(mockMongoEvalDatasetCollection.aggregate).toHaveBeenCalledWith( - expect.arrayContaining([{ $skip: 0 }, { $limit: 10 }]) + expect.arrayContaining([ + { $match: { teamId: new Types.ObjectId(validTeamId) } }, + { $sort: { createTime: -1 } }, + { $skip: 0 }, + { $limit: 10 } + ]) ); }); }); @@ -185,8 +250,6 @@ describe('EvalDatasetCollection List API', () => { expect(mockMongoEvalDatasetCollection.aggregate).toHaveBeenCalledWith( expect.arrayContaining([{ $match: expectedMatch }]) ); - - expect(mockMongoEvalDatasetCollection.countDocuments).toHaveBeenCalledWith(expectedMatch); }); it('should trim search key before processing', async () => { @@ -267,12 +330,11 @@ describe('EvalDatasetCollection List API', () => { _id: 1, name: 1, description: 1, + tmbId: 1, createTime: 1, updateTime: 1, - teamMember: { - avatar: 1, - name: 1 - } + creatorAvatar: '$teamMember.avatar', + creatorName: '$teamMember.name' } } ]); @@ -317,7 +379,13 @@ describe('EvalDatasetCollection List API', () => { createTime: expect.any(Date), updateTime: expect.any(Date), creatorAvatar: 'avatar1.jpg', - creatorName: 'User One' + creatorName: 'User One', + permission: expect.objectContaining({ + hasReadPer: true, + hasWritePer: true, + hasManagePer: true + }), + private: expect.any(Boolean) }, { _id: '65f5b5b5b5b5b5b5b5b5b5b2', @@ -327,7 +395,13 @@ describe('EvalDatasetCollection List API', () => { createTime: expect.any(Date), updateTime: expect.any(Date), creatorAvatar: 'avatar2.jpg', - creatorName: 'User Two' + creatorName: 'User Two', + permission: expect.objectContaining({ + hasReadPer: true, + hasWritePer: true, + hasManagePer: true + }), + private: expect.any(Boolean) } ] }); @@ -362,7 +436,13 @@ describe('EvalDatasetCollection List API', () => { createTime: expect.any(Date), updateTime: expect.any(Date), creatorAvatar: undefined, - creatorName: undefined + creatorName: undefined, + permission: expect.objectContaining({ + hasReadPer: true, + hasWritePer: true, + hasManagePer: true + }), + private: expect.any(Boolean) }); }); diff --git a/test/cases/pages/api/core/evaluation/dataset/collection/update.test.ts b/test/cases/pages/api/core/evaluation/dataset/collection/update.test.ts index 15d60f808039..a087092bbedd 100644 --- a/test/cases/pages/api/core/evaluation/dataset/collection/update.test.ts +++ b/test/cases/pages/api/core/evaluation/dataset/collection/update.test.ts @@ -1,23 +1,23 @@ import { describe, expect, it, vi, beforeEach } from 'vitest'; import { handler_test } from '@/pages/api/core/evaluation/dataset/collection/update'; -import { authUserPer } from '@fastgpt/service/support/permission/user/auth'; +import { authEvaluationDatasetWrite } from '@fastgpt/service/core/evaluation/common'; import { mongoSessionRun } from '@fastgpt/service/common/mongo/sessionRun'; import { MongoEvalDatasetCollection } from '@fastgpt/service/core/evaluation/dataset/evalDatasetCollectionSchema'; -import { WritePermissionVal } from '@fastgpt/global/support/permission/constant'; -vi.mock('@fastgpt/service/support/permission/user/auth'); +vi.mock('@fastgpt/service/core/evaluation/common'); vi.mock('@fastgpt/service/common/mongo/sessionRun'); vi.mock('@fastgpt/service/core/evaluation/dataset/evalDatasetCollectionSchema', () => ({ MongoEvalDatasetCollection: { findOne: vi.fn(), updateOne: vi.fn() - } + }, + EvalDatasetCollectionName: 'eval_dataset_collections' })); vi.mock('@fastgpt/service/support/user/audit/util', () => ({ addAuditLog: vi.fn() })); -const mockAuthUserPer = vi.mocked(authUserPer); +const mockAuthEvaluationDatasetWrite = vi.mocked(authEvaluationDatasetWrite); const mockMongoSessionRun = vi.mocked(mongoSessionRun); const mockMongoEvalDatasetCollection = vi.mocked(MongoEvalDatasetCollection); @@ -35,7 +35,7 @@ describe('EvalDatasetCollection Update API', () => { beforeEach(() => { vi.clearAllMocks(); - mockAuthUserPer.mockResolvedValue({ + mockAuthEvaluationDatasetWrite.mockResolvedValue({ teamId: validTeamId, tmbId: validTmbId }); @@ -207,17 +207,16 @@ describe('EvalDatasetCollection Update API', () => { await handler_test(req as any); - expect(mockAuthUserPer).toHaveBeenCalledWith({ + expect(mockAuthEvaluationDatasetWrite).toHaveBeenCalledWith(mockCollectionId, { req, authToken: true, - authApiKey: true, - per: WritePermissionVal + authApiKey: true }); }); it('should propagate authentication errors', async () => { const authError = new Error('Authentication failed'); - mockAuthUserPer.mockRejectedValue(authError); + mockAuthEvaluationDatasetWrite.mockRejectedValue(authError); const req = { body: { @@ -227,7 +226,7 @@ describe('EvalDatasetCollection Update API', () => { } }; - await expect(handler_test(req as any)).rejects.toBe(authError); + await expect(handler_test(req as any)).rejects.toThrow('Authentication failed'); }); }); diff --git a/test/cases/pages/api/core/evaluation/dataset/data/create.test.ts b/test/cases/pages/api/core/evaluation/dataset/data/create.test.ts index 24fdf36a35dc..3cb63a64664a 100644 --- a/test/cases/pages/api/core/evaluation/dataset/data/create.test.ts +++ b/test/cases/pages/api/core/evaluation/dataset/data/create.test.ts @@ -1,16 +1,14 @@ import { describe, expect, it, vi, beforeEach } from 'vitest'; import { handler_test } from '@/pages/api/core/evaluation/dataset/data/create'; -import { authUserPer } from '@fastgpt/service/support/permission/user/auth'; +import { authEvaluationDatasetDataCreate } from '@fastgpt/service/core/evaluation/common'; import { mongoSessionRun } from '@fastgpt/service/common/mongo/sessionRun'; import { MongoEvalDatasetData } from '@fastgpt/service/core/evaluation/dataset/evalDatasetDataSchema'; -import { MongoEvalDatasetCollection } from '@fastgpt/service/core/evaluation/dataset/evalDatasetCollectionSchema'; -import { WritePermissionVal } from '@fastgpt/global/support/permission/constant'; import { EvalDatasetDataCreateFromEnum, EvalDatasetDataKeyEnum } from '@fastgpt/global/core/evaluation/dataset/constants'; -vi.mock('@fastgpt/service/support/permission/user/auth'); +vi.mock('@fastgpt/service/core/evaluation/common'); vi.mock('@fastgpt/service/common/mongo/sessionRun'); vi.mock('@fastgpt/service/core/evaluation/dataset/evalDatasetDataSchema', () => ({ MongoEvalDatasetData: { @@ -20,13 +18,16 @@ vi.mock('@fastgpt/service/core/evaluation/dataset/evalDatasetDataSchema', () => vi.mock('@fastgpt/service/core/evaluation/dataset/evalDatasetCollectionSchema', () => ({ MongoEvalDatasetCollection: { findOne: vi.fn() - } + }, + EvalDatasetCollectionName: 'eval_dataset_collections' })); vi.mock('@fastgpt/service/support/user/audit/util', () => ({ addAuditLog: vi.fn() })); -const mockAuthUserPer = vi.mocked(authUserPer); +import { MongoEvalDatasetCollection } from '@fastgpt/service/core/evaluation/dataset/evalDatasetCollectionSchema'; + +const mockAuthEvaluationDatasetDataCreate = vi.mocked(authEvaluationDatasetDataCreate); const mockMongoSessionRun = vi.mocked(mongoSessionRun); const mockMongoEvalDatasetData = vi.mocked(MongoEvalDatasetData); const mockMongoEvalDatasetCollection = vi.mocked(MongoEvalDatasetCollection); @@ -40,13 +41,15 @@ describe('EvalDatasetData Create API', () => { beforeEach(() => { vi.clearAllMocks(); - mockAuthUserPer.mockResolvedValue({ + mockAuthEvaluationDatasetDataCreate.mockResolvedValue({ teamId: validTeamId, tmbId: validTmbId }); + // Mock collection exists mockMongoEvalDatasetCollection.findOne.mockResolvedValue({ _id: validCollectionId, + name: 'Test Collection', teamId: validTeamId } as any); @@ -272,7 +275,7 @@ describe('EvalDatasetData Create API', () => { }); describe('Authentication and Authorization', () => { - it('should call authUserPer with correct parameters', async () => { + it('should call authEvaluationDatasetDataCreate with correct parameters', async () => { const req = { body: { collectionId: validCollectionId, @@ -283,17 +286,16 @@ describe('EvalDatasetData Create API', () => { await handler_test(req as any); - expect(mockAuthUserPer).toHaveBeenCalledWith({ + expect(mockAuthEvaluationDatasetDataCreate).toHaveBeenCalledWith(validCollectionId, { req, authToken: true, - authApiKey: true, - per: WritePermissionVal + authApiKey: true }); }); it('should propagate authentication errors', async () => { const authError = new Error('Authentication failed'); - mockAuthUserPer.mockRejectedValue(authError); + mockAuthEvaluationDatasetDataCreate.mockRejectedValue(authError); const req = { body: { @@ -303,29 +305,13 @@ describe('EvalDatasetData Create API', () => { } }; - await expect(handler_test(req as any)).rejects.toBe(authError); + await expect(handler_test(req as any)).rejects.toThrow('Authentication failed'); }); }); describe('Collection Validation', () => { - it('should verify collection exists and belongs to team', async () => { - const req = { - body: { - collectionId: validCollectionId, - userInput: 'Test input', - expectedOutput: 'Test output' - } - }; - - await handler_test(req as any); - - expect(mockMongoEvalDatasetCollection.findOne).toHaveBeenCalledWith({ - _id: validCollectionId, - teamId: validTeamId - }); - }); - it('should reject when collection does not exist', async () => { + // Mock collection not found mockMongoEvalDatasetCollection.findOne.mockResolvedValue(null); const req = { @@ -336,12 +322,13 @@ describe('EvalDatasetData Create API', () => { } }; - await expect(handler_test(req as any)).rejects.toEqual( + await expect(handler_test(req as any)).rejects.toBe( 'Dataset collection not found or access denied' ); }); it('should reject when collection belongs to different team', async () => { + // Mock collection not found (same as collection not existing for this team) mockMongoEvalDatasetCollection.findOne.mockResolvedValue(null); const req = { @@ -352,7 +339,7 @@ describe('EvalDatasetData Create API', () => { } }; - await expect(handler_test(req as any)).rejects.toEqual( + await expect(handler_test(req as any)).rejects.toBe( 'Dataset collection not found or access denied' ); }); @@ -586,6 +573,12 @@ describe('EvalDatasetData Create API', () => { }); it('should propagate database creation errors', async () => { + // Reset auth mock to succeed for this test + mockAuthEvaluationDatasetDataCreate.mockResolvedValue({ + teamId: validTeamId, + tmbId: validTmbId + }); + const dbError = new Error('Database connection failed'); mockMongoSessionRun.mockRejectedValue(dbError); diff --git a/test/cases/pages/api/core/evaluation/dataset/data/delete.test.ts b/test/cases/pages/api/core/evaluation/dataset/data/delete.test.ts index 33988f07944e..805bae38761f 100644 --- a/test/cases/pages/api/core/evaluation/dataset/data/delete.test.ts +++ b/test/cases/pages/api/core/evaluation/dataset/data/delete.test.ts @@ -1,36 +1,40 @@ import { describe, expect, it, vi, beforeEach } from 'vitest'; import { handler_test } from '@/pages/api/core/evaluation/dataset/data/delete'; -import { authUserPer } from '@fastgpt/service/support/permission/user/auth'; +import { authEvaluationDatasetDataUpdateById } from '@fastgpt/service/core/evaluation/common'; import { mongoSessionRun } from '@fastgpt/service/common/mongo/sessionRun'; import { MongoEvalDatasetData } from '@fastgpt/service/core/evaluation/dataset/evalDatasetDataSchema'; -import { MongoEvalDatasetCollection } from '@fastgpt/service/core/evaluation/dataset/evalDatasetCollectionSchema'; -import { WritePermissionVal } from '@fastgpt/global/support/permission/constant'; import { removeEvalDatasetDataQualityJobsRobust, checkEvalDatasetDataQualityJobActive } from '@fastgpt/service/core/evaluation/dataset/dataQualityMq'; import { addLog } from '@fastgpt/service/common/system/log'; +import { MongoEvalDatasetCollection } from '@fastgpt/service/core/evaluation/dataset/evalDatasetCollectionSchema'; -vi.mock('@fastgpt/service/support/permission/user/auth'); +vi.mock('@fastgpt/service/core/evaluation/common'); vi.mock('@fastgpt/service/common/mongo/sessionRun'); vi.mock('@fastgpt/service/core/evaluation/dataset/evalDatasetDataSchema', () => ({ MongoEvalDatasetData: { - findById: vi.fn(), + findById: vi.fn(() => ({ + session: vi.fn() + })), deleteOne: vi.fn() } })); +vi.mock('@fastgpt/service/core/evaluation/dataset/dataQualityMq'); +vi.mock('@fastgpt/service/common/system/log'); vi.mock('@fastgpt/service/core/evaluation/dataset/evalDatasetCollectionSchema', () => ({ MongoEvalDatasetCollection: { - findOne: vi.fn() - } + findOne: vi.fn(() => ({ + session: vi.fn() + })) + }, + EvalDatasetCollectionName: 'eval_dataset_collections' })); -vi.mock('@fastgpt/service/core/evaluation/dataset/dataQualityMq'); -vi.mock('@fastgpt/service/common/system/log'); vi.mock('@fastgpt/service/support/user/audit/util', () => ({ addAuditLog: vi.fn() })); -const mockAuthUserPer = vi.mocked(authUserPer); +const mockAuthEvaluationDatasetDataUpdateById = vi.mocked(authEvaluationDatasetDataUpdateById); const mockMongoSessionRun = vi.mocked(mongoSessionRun); const mockMongoEvalDatasetData = vi.mocked(MongoEvalDatasetData); const mockMongoEvalDatasetCollection = vi.mocked(MongoEvalDatasetCollection); @@ -45,40 +49,45 @@ describe('EvalDatasetData Delete API', () => { const validTmbId = 'tmb123'; const validDataId = '65f5b5b5b5b5b5b5b5b5b5b5'; const validCollectionId = '65f5b5b5b5b5b5b5b5b5b5b6'; - const mockSession = { id: 'session-123' }; - const mockExistingData = { - _id: validDataId, - datasetId: validCollectionId, - userInput: 'Test input', - expectedOutput: 'Test output' - }; - - const mockCollection = { - _id: validCollectionId, - teamId: validTeamId, - name: 'Test Collection' - }; beforeEach(() => { vi.clearAllMocks(); - mockAuthUserPer.mockResolvedValue({ + mockAuthEvaluationDatasetDataUpdateById.mockResolvedValue({ teamId: validTeamId, - tmbId: validTmbId + tmbId: validTmbId, + collectionId: validCollectionId }); mockMongoSessionRun.mockImplementation(async (callback) => { return callback(mockSession as any); }); - mockMongoEvalDatasetData.findById.mockReturnValue({ - session: vi.fn().mockResolvedValue(mockExistingData) - } as any); + // Mock findById chain for existing data + const mockDataDocument = { + _id: validDataId, + datasetId: validCollectionId, + userInput: 'test input', + expectedOutput: 'test output' + }; + + const mockFindByIdResult = { + session: vi.fn().mockResolvedValue(mockDataDocument) + }; + mockMongoEvalDatasetData.findById.mockReturnValue(mockFindByIdResult as any); - mockMongoEvalDatasetCollection.findOne.mockReturnValue({ - session: vi.fn().mockResolvedValue(mockCollection) - } as any); + // Mock findOne chain for collection + const mockCollectionDocument = { + _id: validCollectionId, + name: 'Test Collection', + teamId: validTeamId + }; + + const mockFindOneResult = { + session: vi.fn().mockResolvedValue(mockCollectionDocument) + }; + mockMongoEvalDatasetCollection.findOne.mockReturnValue(mockFindOneResult as any); mockCheckEvalDatasetDataQualityJobActive.mockResolvedValue(false); mockMongoEvalDatasetData.deleteOne.mockResolvedValue({ deletedCount: 1 } as any); @@ -98,6 +107,16 @@ describe('EvalDatasetData Delete API', () => { ); }); + it('should reject when dataId is empty string', async () => { + const req = { + query: { dataId: '' } + }; + + await expect(handler_test(req as any)).rejects.toEqual( + 'dataId is required and must be a string' + ); + }); + it('should reject when dataId is null', async () => { const req = { query: { dataId: null } @@ -128,9 +147,9 @@ describe('EvalDatasetData Delete API', () => { ); }); - it('should reject when dataId is empty string', async () => { + it('should reject when dataId is whitespace only', async () => { const req = { - query: { dataId: '' } + query: { dataId: ' ' } }; await expect(handler_test(req as any)).rejects.toEqual( @@ -140,100 +159,48 @@ describe('EvalDatasetData Delete API', () => { }); describe('Authentication and Authorization', () => { - it('should call authUserPer with correct parameters', async () => { + it('should call authEvaluationDatasetDataUpdateById with correct parameters', async () => { const req = { query: { dataId: validDataId } }; await handler_test(req as any); - expect(mockAuthUserPer).toHaveBeenCalledWith({ + expect(mockAuthEvaluationDatasetDataUpdateById).toHaveBeenCalledWith(validDataId, { req, authToken: true, - authApiKey: true, - per: WritePermissionVal + authApiKey: true }); }); it('should propagate authentication errors', async () => { const authError = new Error('Authentication failed'); - mockAuthUserPer.mockRejectedValue(authError); + mockAuthEvaluationDatasetDataUpdateById.mockRejectedValue(authError); const req = { query: { dataId: validDataId } }; - await expect(handler_test(req as any)).rejects.toBe(authError); + await expect(handler_test(req as any)).rejects.toThrow('Authentication failed'); }); }); describe('Data Validation', () => { - it('should verify data exists', async () => { - const req = { - query: { dataId: validDataId } - }; - - await handler_test(req as any); - - expect(mockMongoEvalDatasetData.findById).toHaveBeenCalledWith(validDataId); - }); - it('should reject when data does not exist', async () => { - mockMongoEvalDatasetData.findById.mockReturnValue({ - session: vi.fn().mockResolvedValue(null) - } as any); - - const req = { - query: { dataId: validDataId } - }; - - await expect(handler_test(req as any)).rejects.toEqual('Dataset data not found'); - }); - - it('should verify collection exists and belongs to team', async () => { - const req = { - query: { dataId: validDataId } - }; - - await handler_test(req as any); - - expect(mockMongoEvalDatasetCollection.findOne).toHaveBeenCalledWith({ - _id: validCollectionId, - teamId: validTeamId - }); - }); - - it('should reject when collection does not exist', async () => { - mockMongoEvalDatasetCollection.findOne.mockReturnValue({ - session: vi.fn().mockResolvedValue(null) - } as any); - - const req = { - query: { dataId: validDataId } - }; - - await expect(handler_test(req as any)).rejects.toEqual( - 'Access denied or dataset collection not found' + mockAuthEvaluationDatasetDataUpdateById.mockRejectedValue( + new Error('Dataset data not found') ); - }); - - it('should reject when collection belongs to different team', async () => { - mockMongoEvalDatasetCollection.findOne.mockReturnValue({ - session: vi.fn().mockResolvedValue(null) - } as any); const req = { query: { dataId: validDataId } }; - await expect(handler_test(req as any)).rejects.toEqual( - 'Access denied or dataset collection not found' - ); + await expect(handler_test(req as any)).rejects.toThrow('Dataset data not found'); }); }); describe('Quality Job Management', () => { - it('should check for active quality evaluation jobs', async () => { + it('should check for active quality job', async () => { const req = { query: { dataId: validDataId } }; @@ -243,7 +210,7 @@ describe('EvalDatasetData Delete API', () => { expect(mockCheckEvalDatasetDataQualityJobActive).toHaveBeenCalledWith(validDataId); }); - it('should remove active quality job before deletion', async () => { + it('should remove active quality job if exists', async () => { mockCheckEvalDatasetDataQualityJobActive.mockResolvedValue(true); const req = { @@ -310,62 +277,32 @@ describe('EvalDatasetData Delete API', () => { }); describe('Data Deletion', () => { - it('should delete data with correct parameters', async () => { - const req = { - query: { dataId: validDataId } - }; - - const result = await handler_test(req as any); - - expect(mockMongoEvalDatasetData.deleteOne).toHaveBeenCalledWith( - { _id: validDataId }, - { session: mockSession } - ); - expect(result).toBe('success'); - }); - - it('should log successful deletion', async () => { + it('should delete data using MongoDB session', async () => { const req = { query: { dataId: validDataId } }; await handler_test(req as any); - expect(mockAddLog.info).toHaveBeenCalledWith('Evaluation dataset data deleted successfully', { - dataId: validDataId, - datasetId: validCollectionId, - teamId: validTeamId - }); - }); - - it('should use MongoDB session for all operations', async () => { - const req = { - query: { dataId: validDataId } - }; - - await handler_test(req as any); - - expect(mockMongoEvalDatasetData.findById().session).toHaveBeenCalledWith(mockSession); - expect(mockMongoEvalDatasetCollection.findOne().session).toHaveBeenCalledWith(mockSession); expect(mockMongoEvalDatasetData.deleteOne).toHaveBeenCalledWith( { _id: validDataId }, { session: mockSession } ); }); - it('should return success message', async () => { + it('should return success when deletion completes', async () => { const req = { query: { dataId: validDataId } }; const result = await handler_test(req as any); + expect(result).toBe('success'); - expect(typeof result).toBe('string'); }); }); describe('Session Management', () => { - it('should wrap operations in MongoDB session', async () => { + it('should use MongoDB session for all operations', async () => { const req = { query: { dataId: validDataId } }; @@ -374,7 +311,9 @@ describe('EvalDatasetData Delete API', () => { expect(mockMongoSessionRun).toHaveBeenCalledWith(expect.any(Function)); }); + }); + describe('Error Handling', () => { it('should propagate session errors', async () => { const sessionError = new Error('Session failed'); mockMongoSessionRun.mockRejectedValue(sessionError); @@ -383,135 +322,18 @@ describe('EvalDatasetData Delete API', () => { query: { dataId: validDataId } }; - await expect(handler_test(req as any)).rejects.toBe(sessionError); - }); - }); - - describe('Error Handling', () => { - it('should propagate database findById errors', async () => { - const dbError = new Error('Database connection failed'); - mockMongoEvalDatasetData.findById.mockReturnValue({ - session: vi.fn().mockRejectedValue(dbError) - } as any); - - const req = { - query: { dataId: validDataId } - }; - - await expect(handler_test(req as any)).rejects.toBe(dbError); - }); - - it('should propagate collection findOne errors', async () => { - const dbError = new Error('Database connection failed'); - mockMongoEvalDatasetCollection.findOne.mockReturnValue({ - session: vi.fn().mockRejectedValue(dbError) - } as any); - - const req = { - query: { dataId: validDataId } - }; - - await expect(handler_test(req as any)).rejects.toBe(dbError); + await expect(handler_test(req as any)).rejects.toThrow('Session failed'); }); - it('should propagate deleteOne errors', async () => { - const dbError = new Error('Database deletion failed'); - mockMongoEvalDatasetData.deleteOne.mockRejectedValue(dbError); + it('should propagate delete errors', async () => { + const deleteError = new Error('Delete failed'); + mockMongoEvalDatasetData.deleteOne.mockRejectedValue(deleteError); const req = { query: { dataId: validDataId } }; - await expect(handler_test(req as any)).rejects.toBe(dbError); - }); - - it('should propagate quality job check errors', async () => { - const jobError = new Error('Quality job check failed'); - mockCheckEvalDatasetDataQualityJobActive.mockRejectedValue(jobError); - - const req = { - query: { dataId: validDataId } - }; - - await expect(handler_test(req as any)).rejects.toBe(jobError); - }); - }); - - describe('Edge Cases', () => { - it('should handle valid ObjectId format for dataId', async () => { - const validObjectId = '507f1f77bcf86cd799439011'; - const req = { - query: { dataId: validObjectId } - }; - - const result = await handler_test(req as any); - expect(result).toBe('success'); - }); - - it('should handle data with minimal fields', async () => { - const minimalData = { - _id: validDataId, - datasetId: validCollectionId - }; - - mockMongoEvalDatasetData.findById.mockReturnValue({ - session: vi.fn().mockResolvedValue(minimalData) - } as any); - - const req = { - query: { dataId: validDataId } - }; - - const result = await handler_test(req as any); - expect(result).toBe('success'); - }); - - it('should handle collection with minimal fields', async () => { - const minimalCollection = { - _id: validCollectionId, - teamId: validTeamId - }; - - mockMongoEvalDatasetCollection.findOne.mockReturnValue({ - session: vi.fn().mockResolvedValue(minimalCollection) - } as any); - - const req = { - query: { dataId: validDataId } - }; - - const result = await handler_test(req as any); - expect(result).toBe('success'); - }); - - it('should handle whitespace in dataId', async () => { - const req = { - query: { dataId: ' ' } - }; - - await expect(handler_test(req as any)).rejects.toEqual( - 'dataId is required and must be a string' - ); - }); - - it('should handle array dataId', async () => { - const req = { - query: { dataId: [validDataId] } - }; - - await expect(handler_test(req as any)).rejects.toEqual( - 'dataId is required and must be a string' - ); - }); - - it('should handle object dataId', async () => { - const req = { - query: { dataId: { id: validDataId } } - }; - - await expect(handler_test(req as any)).rejects.toEqual( - 'dataId is required and must be a string' - ); + await expect(handler_test(req as any)).rejects.toThrow('Delete failed'); }); }); @@ -526,7 +348,11 @@ describe('EvalDatasetData Delete API', () => { const result = await handler_test(req as any); // Verify complete flow - expect(mockAuthUserPer).toHaveBeenCalled(); + expect(mockAuthEvaluationDatasetDataUpdateById).toHaveBeenCalledWith(validDataId, { + req, + authToken: true, + authApiKey: true + }); expect(mockMongoEvalDatasetData.findById).toHaveBeenCalled(); expect(mockMongoEvalDatasetCollection.findOne).toHaveBeenCalled(); expect(mockCheckEvalDatasetDataQualityJobActive).toHaveBeenCalled(); @@ -545,7 +371,11 @@ describe('EvalDatasetData Delete API', () => { const result = await handler_test(req as any); // Verify complete flow - expect(mockAuthUserPer).toHaveBeenCalled(); + expect(mockAuthEvaluationDatasetDataUpdateById).toHaveBeenCalledWith(validDataId, { + req, + authToken: true, + authApiKey: true + }); expect(mockMongoEvalDatasetData.findById).toHaveBeenCalled(); expect(mockMongoEvalDatasetCollection.findOne).toHaveBeenCalled(); expect(mockCheckEvalDatasetDataQualityJobActive).toHaveBeenCalled(); @@ -553,19 +383,5 @@ describe('EvalDatasetData Delete API', () => { expect(mockMongoEvalDatasetData.deleteOne).toHaveBeenCalled(); expect(result).toBe('success'); }); - - it('should maintain transaction integrity on session failure', async () => { - const sessionError = new Error('Session rollback'); - mockMongoEvalDatasetData.deleteOne.mockRejectedValue(sessionError); - - const req = { - query: { dataId: validDataId } - }; - - await expect(handler_test(req as any)).rejects.toBe(sessionError); - - // Verify session was used for all operations - expect(mockMongoSessionRun).toHaveBeenCalled(); - }); }); }); diff --git a/test/cases/pages/api/core/evaluation/dataset/data/fileId.test.ts b/test/cases/pages/api/core/evaluation/dataset/data/fileId.test.ts index a6c11298e285..ef801ba07fd3 100644 --- a/test/cases/pages/api/core/evaluation/dataset/data/fileId.test.ts +++ b/test/cases/pages/api/core/evaluation/dataset/data/fileId.test.ts @@ -1,6 +1,7 @@ import { describe, expect, it, vi, beforeEach } from 'vitest'; import { handler_test } from '@/pages/api/core/evaluation/dataset/data/fileId'; import { authEvalDatasetCollectionFile } from '@fastgpt/service/support/permission/evaluation/auth'; +import { authEvaluationDatasetDataWrite } from '@fastgpt/service/core/evaluation/common'; import { mongoSessionRun } from '@fastgpt/service/common/mongo/sessionRun'; import { MongoEvalDatasetData } from '@fastgpt/service/core/evaluation/dataset/evalDatasetDataSchema'; import { MongoEvalDatasetCollection } from '@fastgpt/service/core/evaluation/dataset/evalDatasetCollectionSchema'; @@ -14,17 +15,25 @@ import { import { addEvalDatasetDataQualityJob } from '@fastgpt/service/core/evaluation/dataset/dataQualityMq'; vi.mock('@fastgpt/service/support/permission/evaluation/auth'); +vi.mock('@fastgpt/service/core/evaluation/common'); vi.mock('@fastgpt/service/common/mongo/sessionRun'); vi.mock('@fastgpt/service/core/evaluation/dataset/evalDatasetDataSchema', () => ({ MongoEvalDatasetData: { insertMany: vi.fn() } })); -vi.mock('@fastgpt/service/core/evaluation/dataset/evalDatasetCollectionSchema', () => ({ - MongoEvalDatasetCollection: { - findById: vi.fn() +vi.mock( + '@fastgpt/service/core/evaluation/dataset/evalDatasetCollectionSchema', + async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + MongoEvalDatasetCollection: { + findById: vi.fn() + } + }; } -})); +); vi.mock('@fastgpt/service/common/file/gridfs/controller', () => ({ readFileContentFromMongo: vi.fn() })); @@ -36,6 +45,7 @@ vi.mock('@fastgpt/service/support/user/audit/util', () => ({ })); const mockAuthEvalDatasetCollectionFile = vi.mocked(authEvalDatasetCollectionFile); +const mockAuthEvaluationDatasetDataWrite = vi.mocked(authEvaluationDatasetDataWrite); const mockMongoSessionRun = vi.mocked(mongoSessionRun); const mockMongoEvalDatasetData = vi.mocked(MongoEvalDatasetData); const mockMongoEvalDatasetCollection = vi.mocked(MongoEvalDatasetCollection); @@ -72,10 +82,11 @@ describe('EvalDatasetData FileId Import API', () => { } } as any); - mockMongoEvalDatasetCollection.findById.mockResolvedValue({ - _id: validCollectionId, - teamId: validTeamId - } as any); + mockAuthEvaluationDatasetDataWrite.mockResolvedValue({ + teamId: validTeamId, + tmbId: validTmbId, + collectionId: validCollectionId + }); // Don't set default CSV content - let each test set its own mockReadFileContentFromMongo.mockResolvedValue({ @@ -87,6 +98,11 @@ describe('EvalDatasetData FileId Import API', () => { }); mockMongoEvalDatasetData.insertMany.mockResolvedValue(mockInsertedRecords as any); + mockMongoEvalDatasetCollection.findById.mockResolvedValue({ + _id: validCollectionId, + teamId: validTeamId, + name: 'Test Collection' + } as any); mockAddEvalDatasetDataQualityJob.mockResolvedValue({} as any); }); @@ -329,8 +345,10 @@ describe('EvalDatasetData FileId Import API', () => { }); describe('Dataset Collection Validation', () => { - it('should reject when dataset collection does not exist', async () => { - mockMongoEvalDatasetCollection.findById.mockResolvedValue(null); + it('should reject when dataset collection access is denied', async () => { + mockAuthEvaluationDatasetDataWrite.mockRejectedValue( + new Error('Dataset collection not found or access denied') + ); const req = { body: { @@ -340,15 +358,15 @@ describe('EvalDatasetData FileId Import API', () => { } }; - const result = await handler_test(req as any); - expect(result).toBe('Evaluation dataset collection not found'); + await expect(handler_test(req as any)).rejects.toThrow( + 'Dataset collection not found or access denied' + ); }); it('should reject when dataset collection belongs to different team', async () => { - mockMongoEvalDatasetCollection.findById.mockResolvedValue({ - _id: validCollectionId, - teamId: 'different-team' - } as any); + mockAuthEvaluationDatasetDataWrite.mockRejectedValue( + new Error('No permission to access this dataset collection') + ); const req = { body: { @@ -358,8 +376,9 @@ describe('EvalDatasetData FileId Import API', () => { } }; - const result = await handler_test(req as any); - expect(result).toBe('No permission to access this dataset collection'); + await expect(handler_test(req as any)).rejects.toThrow( + 'No permission to access this dataset collection' + ); }); }); diff --git a/test/cases/pages/api/core/evaluation/dataset/data/list.test.ts b/test/cases/pages/api/core/evaluation/dataset/data/list.test.ts index 2a6cc950900d..6f66c5911cfc 100644 --- a/test/cases/pages/api/core/evaluation/dataset/data/list.test.ts +++ b/test/cases/pages/api/core/evaluation/dataset/data/list.test.ts @@ -1,28 +1,20 @@ import { describe, expect, it, vi, beforeEach } from 'vitest'; import { handler_test } from '@/pages/api/core/evaluation/dataset/data/list'; -import { authUserPer } from '@fastgpt/service/support/permission/user/auth'; +import { authEvaluationDatasetDataRead } from '@fastgpt/service/core/evaluation/common'; import { MongoEvalDatasetData } from '@fastgpt/service/core/evaluation/dataset/evalDatasetDataSchema'; -import { MongoEvalDatasetCollection } from '@fastgpt/service/core/evaluation/dataset/evalDatasetCollectionSchema'; -import { ReadPermissionVal } from '@fastgpt/global/support/permission/constant'; import { Types } from '@fastgpt/service/common/mongo'; import { EvalDatasetDataKeyEnum } from '@fastgpt/global/core/evaluation/dataset/constants'; -vi.mock('@fastgpt/service/support/permission/user/auth'); +vi.mock('@fastgpt/service/core/evaluation/common'); vi.mock('@fastgpt/service/core/evaluation/dataset/evalDatasetDataSchema', () => ({ MongoEvalDatasetData: { aggregate: vi.fn(), countDocuments: vi.fn() } })); -vi.mock('@fastgpt/service/core/evaluation/dataset/evalDatasetCollectionSchema', () => ({ - MongoEvalDatasetCollection: { - findOne: vi.fn() - } -})); -const mockAuthUserPer = vi.mocked(authUserPer); +const mockAuthEvaluationDatasetDataRead = vi.mocked(authEvaluationDatasetDataRead); const mockMongoEvalDatasetData = vi.mocked(MongoEvalDatasetData); -const mockMongoEvalDatasetCollection = vi.mocked(MongoEvalDatasetCollection); describe('EvalDatasetData List API', () => { const validTeamId = '65f5b5b5b5b5b5b5b5b5b5b0'; @@ -59,16 +51,12 @@ describe('EvalDatasetData List API', () => { beforeEach(() => { vi.clearAllMocks(); - mockAuthUserPer.mockResolvedValue({ + mockAuthEvaluationDatasetDataRead.mockResolvedValue({ teamId: validTeamId, - tmbId: validTmbId + tmbId: validTmbId, + collectionId: validCollectionId }); - mockMongoEvalDatasetCollection.findOne.mockResolvedValue({ - _id: validCollectionId, - teamId: validTeamId - } as any); - mockMongoEvalDatasetData.aggregate.mockResolvedValue(mockDataItems); mockMongoEvalDatasetData.countDocuments.mockResolvedValue(2); }); @@ -108,49 +96,37 @@ describe('EvalDatasetData List API', () => { }); describe('Authentication and Authorization', () => { - it('should call authUserPer with correct parameters', async () => { + it('should call authEvaluationDatasetDataRead with correct parameters', async () => { const req = { body: { collectionId: validCollectionId, pageNum: 1, pageSize: 10 } }; await handler_test(req as any); - expect(mockAuthUserPer).toHaveBeenCalledWith({ + expect(mockAuthEvaluationDatasetDataRead).toHaveBeenCalledWith(validCollectionId, { req, authToken: true, - authApiKey: true, - per: ReadPermissionVal + authApiKey: true }); }); it('should propagate authentication errors', async () => { const authError = new Error('Authentication failed'); - mockAuthUserPer.mockRejectedValue(authError); + mockAuthEvaluationDatasetDataRead.mockRejectedValue(authError); const req = { body: { collectionId: validCollectionId, pageNum: 1, pageSize: 10 } }; - await expect(handler_test(req as any)).rejects.toBe(authError); + await expect(handler_test(req as any)).rejects.toThrow('Authentication failed'); }); }); describe('Collection Validation', () => { - it('should verify collection exists and belongs to team', async () => { - const req = { - body: { collectionId: validCollectionId, pageNum: 1, pageSize: 10 } - }; - - await handler_test(req as any); - - expect(mockMongoEvalDatasetCollection.findOne).toHaveBeenCalledWith({ - _id: new Types.ObjectId(validCollectionId), - teamId: new Types.ObjectId(validTeamId) - }); - }); - it('should reject when collection does not exist', async () => { - mockMongoEvalDatasetCollection.findOne.mockResolvedValue(null); + mockAuthEvaluationDatasetDataRead.mockRejectedValue( + new Error('Collection not found or access denied') + ); const req = { body: { collectionId: validCollectionId, pageNum: 1, pageSize: 10 } @@ -162,7 +138,9 @@ describe('EvalDatasetData List API', () => { }); it('should reject when collection belongs to different team', async () => { - mockMongoEvalDatasetCollection.findOne.mockResolvedValue(null); + mockAuthEvaluationDatasetDataRead.mockRejectedValue( + new Error('Collection not found or access denied') + ); const req = { body: { collectionId: validCollectionId, pageNum: 1, pageSize: 10 } @@ -651,15 +629,15 @@ describe('EvalDatasetData List API', () => { await expect(handler_test(req as any)).rejects.toBe(dbError); }); - it('should handle collection findOne errors', async () => { + it('should handle collection access errors', async () => { const dbError = new Error('Collection query failed'); - mockMongoEvalDatasetCollection.findOne.mockRejectedValue(dbError); + mockAuthEvaluationDatasetDataRead.mockRejectedValue(dbError); const req = { body: { collectionId: validCollectionId, pageNum: 1, pageSize: 10 } }; - await expect(handler_test(req as any)).rejects.toBe(dbError); + await expect(handler_test(req as any)).rejects.toThrow('Collection query failed'); }); }); diff --git a/test/cases/pages/api/core/evaluation/dataset/data/qualityAssessment.test.ts b/test/cases/pages/api/core/evaluation/dataset/data/qualityAssessment.test.ts index 747b7c13127f..5f83486c4bd4 100644 --- a/test/cases/pages/api/core/evaluation/dataset/data/qualityAssessment.test.ts +++ b/test/cases/pages/api/core/evaluation/dataset/data/qualityAssessment.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it, vi, beforeEach } from 'vitest'; import { handler_test } from '@/pages/api/core/evaluation/dataset/data/qualityAssessment'; -import { authUserPer } from '@fastgpt/service/support/permission/user/auth'; +import { authEvaluationDatasetDataUpdateById } from '@fastgpt/service/core/evaluation/common'; import { MongoEvalDatasetData } from '@fastgpt/service/core/evaluation/dataset/evalDatasetDataSchema'; import { MongoEvalDatasetCollection } from '@fastgpt/service/core/evaluation/dataset/evalDatasetCollectionSchema'; import { @@ -8,21 +8,17 @@ import { removeEvalDatasetDataQualityJobsRobust, checkEvalDatasetDataQualityJobActive } from '@fastgpt/service/core/evaluation/dataset/dataQualityMq'; -import { WritePermissionVal } from '@fastgpt/global/support/permission/constant'; import { EvalDatasetDataQualityStatusEnum } from '@fastgpt/global/core/evaluation/dataset/constants'; -vi.mock('@fastgpt/service/support/permission/user/auth'); +vi.mock('@fastgpt/service/core/evaluation/common', () => ({ + authEvaluationDatasetDataUpdateById: vi.fn() +})); vi.mock('@fastgpt/service/core/evaluation/dataset/evalDatasetDataSchema', () => ({ MongoEvalDatasetData: { findById: vi.fn(), findByIdAndUpdate: vi.fn() } })); -vi.mock('@fastgpt/service/core/evaluation/dataset/evalDatasetCollectionSchema', () => ({ - MongoEvalDatasetCollection: { - findOne: vi.fn() - } -})); vi.mock('@fastgpt/service/core/evaluation/dataset/dataQualityMq', () => ({ addEvalDatasetDataQualityJob: vi.fn(), removeEvalDatasetDataQualityJobsRobust: vi.fn(), @@ -31,8 +27,13 @@ vi.mock('@fastgpt/service/core/evaluation/dataset/dataQualityMq', () => ({ vi.mock('@fastgpt/service/support/user/audit/util', () => ({ addAuditLog: vi.fn() })); +vi.mock('@fastgpt/service/core/evaluation/dataset/evalDatasetCollectionSchema', () => ({ + MongoEvalDatasetCollection: { + findOne: vi.fn() + } +})); -const mockAuthUserPer = vi.mocked(authUserPer); +const mockAuthEvaluationDatasetDataUpdateById = vi.mocked(authEvaluationDatasetDataUpdateById); const mockMongoEvalDatasetData = vi.mocked(MongoEvalDatasetData); const mockMongoEvalDatasetCollection = vi.mocked(MongoEvalDatasetCollection); const mockAddEvalDatasetDataQualityJob = vi.mocked(addEvalDatasetDataQualityJob); @@ -51,20 +52,28 @@ describe('QualityAssessment API', () => { beforeEach(() => { vi.clearAllMocks(); - mockAuthUserPer.mockResolvedValue({ + mockAuthEvaluationDatasetDataUpdateById.mockResolvedValue({ teamId: validTeamId, - tmbId: validTmbId + tmbId: validTmbId, + collectionId: validCollectionId }); - mockMongoEvalDatasetData.findById.mockResolvedValue({ + // Mock dataset data document + const mockDatasetData = { _id: validDataId, - datasetId: validCollectionId - } as any); - - mockMongoEvalDatasetCollection.findOne.mockResolvedValue({ + datasetId: validCollectionId, + userInput: 'test input', + expectedOutput: 'test output' + }; + mockMongoEvalDatasetData.findById.mockResolvedValue(mockDatasetData as any); + + // Mock collection document + const mockCollection = { _id: validCollectionId, + name: 'Test Collection', teamId: validTeamId - } as any); + }; + mockMongoEvalDatasetCollection.findOne.mockResolvedValue(mockCollection as any); mockCheckEvalDatasetDataQualityJobActive.mockResolvedValue(false); mockAddEvalDatasetDataQualityJob.mockResolvedValue({} as any); @@ -129,7 +138,7 @@ describe('QualityAssessment API', () => { }); describe('Authentication and Authorization', () => { - it('should call authUserPer with correct parameters', async () => { + it('should call authEvaluationDatasetDataUpdateById with correct parameters', async () => { const req = { body: { dataId: validDataId, @@ -139,17 +148,16 @@ describe('QualityAssessment API', () => { await handler_test(req as any); - expect(mockAuthUserPer).toHaveBeenCalledWith({ + expect(mockAuthEvaluationDatasetDataUpdateById).toHaveBeenCalledWith(validDataId, { req, authToken: true, - authApiKey: true, - per: WritePermissionVal + authApiKey: true }); }); it('should propagate authentication errors', async () => { const authError = new Error('Authentication failed'); - mockAuthUserPer.mockRejectedValue(authError); + mockAuthEvaluationDatasetDataUpdateById.mockRejectedValue(authError); const req = { body: { @@ -158,26 +166,15 @@ describe('QualityAssessment API', () => { } }; - await expect(handler_test(req as any)).rejects.toBe(authError); + await expect(handler_test(req as any)).rejects.toThrow('Authentication failed'); }); }); describe('Data Validation', () => { - it('should verify dataset data exists', async () => { - const req = { - body: { - dataId: validDataId, - evalModel: validEvalModel - } - }; - - await handler_test(req as any); - - expect(mockMongoEvalDatasetData.findById).toHaveBeenCalledWith(validDataId); - }); - it('should return error when dataset data not found', async () => { - mockMongoEvalDatasetData.findById.mockResolvedValue(null); + mockAuthEvaluationDatasetDataUpdateById.mockRejectedValue( + new Error('Dataset data not found') + ); const req = { body: { @@ -186,28 +183,13 @@ describe('QualityAssessment API', () => { } }; - const result = await handler_test(req as any); - expect(result).toBe('Dataset data not found'); - }); - - it('should verify collection exists and belongs to team', async () => { - const req = { - body: { - dataId: validDataId, - evalModel: validEvalModel - } - }; - - await handler_test(req as any); - - expect(mockMongoEvalDatasetCollection.findOne).toHaveBeenCalledWith({ - _id: validCollectionId, - teamId: validTeamId - }); + await expect(handler_test(req as any)).rejects.toThrow('Dataset data not found'); }); it('should return error when collection not found', async () => { - mockMongoEvalDatasetCollection.findOne.mockResolvedValue(null); + mockAuthEvaluationDatasetDataUpdateById.mockRejectedValue( + new Error('Dataset collection not found or access denied') + ); const req = { body: { @@ -216,12 +198,15 @@ describe('QualityAssessment API', () => { } }; - const result = await handler_test(req as any); - expect(result).toBe('Dataset collection not found or access denied'); + await expect(handler_test(req as any)).rejects.toThrow( + 'Dataset collection not found or access denied' + ); }); it('should return error when collection belongs to different team', async () => { - mockMongoEvalDatasetCollection.findOne.mockResolvedValue(null); + mockAuthEvaluationDatasetDataUpdateById.mockRejectedValue( + new Error('Dataset collection not found or access denied') + ); const req = { body: { @@ -230,8 +215,9 @@ describe('QualityAssessment API', () => { } }; - const result = await handler_test(req as any); - expect(result).toBe('Dataset collection not found or access denied'); + await expect(handler_test(req as any)).rejects.toThrow( + 'Dataset collection not found or access denied' + ); }); }); @@ -455,10 +441,6 @@ describe('QualityAssessment API', () => { it('should handle very long dataId', async () => { const longDataId = 'a'.repeat(1000); - mockMongoEvalDatasetData.findById.mockResolvedValue({ - _id: longDataId, - datasetId: validCollectionId - } as any); const req = { body: { @@ -511,21 +493,12 @@ describe('QualityAssessment API', () => { vi.clearAllMocks(); // Set up all necessary mocks for this test - mockAuthUserPer.mockResolvedValue({ + mockAuthEvaluationDatasetDataUpdateById.mockResolvedValue({ teamId: validTeamId, - tmbId: validTmbId + tmbId: validTmbId, + collectionId: validCollectionId }); - mockMongoEvalDatasetData.findById.mockResolvedValue({ - _id: validDataId, - datasetId: validCollectionId - } as any); - - mockMongoEvalDatasetCollection.findOne.mockResolvedValue({ - _id: validCollectionId, - teamId: validTeamId - } as any); - mockCheckEvalDatasetDataQualityJobActive.mockResolvedValue(true); mockRemoveEvalDatasetDataQualityJobsRobust.mockResolvedValue(undefined); mockAddEvalDatasetDataQualityJob.mockResolvedValue({} as any); @@ -540,16 +513,10 @@ describe('QualityAssessment API', () => { const result = await handler_test(req as any); - expect(mockAuthUserPer).toHaveBeenCalledWith({ + expect(mockAuthEvaluationDatasetDataUpdateById).toHaveBeenCalledWith(validDataId, { req, authToken: true, - authApiKey: true, - per: WritePermissionVal - }); - expect(mockMongoEvalDatasetData.findById).toHaveBeenCalledWith(validDataId); - expect(mockMongoEvalDatasetCollection.findOne).toHaveBeenCalledWith({ - _id: validCollectionId, - teamId: validTeamId + authApiKey: true }); expect(mockCheckEvalDatasetDataQualityJobActive).toHaveBeenCalledWith(validDataId); expect(mockRemoveEvalDatasetDataQualityJobsRobust).toHaveBeenCalledWith([validDataId]); @@ -579,16 +546,10 @@ describe('QualityAssessment API', () => { const result = await handler_test(req as any); - expect(mockAuthUserPer).toHaveBeenCalledWith({ + expect(mockAuthEvaluationDatasetDataUpdateById).toHaveBeenCalledWith(validDataId, { req, authToken: true, - authApiKey: true, - per: WritePermissionVal - }); - expect(mockMongoEvalDatasetData.findById).toHaveBeenCalledWith(validDataId); - expect(mockMongoEvalDatasetCollection.findOne).toHaveBeenCalledWith({ - _id: validCollectionId, - teamId: validTeamId + authApiKey: true }); expect(mockCheckEvalDatasetDataQualityJobActive).toHaveBeenCalledWith(validDataId); expect(mockRemoveEvalDatasetDataQualityJobsRobust).not.toHaveBeenCalled(); diff --git a/test/cases/pages/api/core/evaluation/dataset/data/update.test.ts b/test/cases/pages/api/core/evaluation/dataset/data/update.test.ts index 9faaf739fd91..37ae7b1a9998 100644 --- a/test/cases/pages/api/core/evaluation/dataset/data/update.test.ts +++ b/test/cases/pages/api/core/evaluation/dataset/data/update.test.ts @@ -1,6 +1,8 @@ import { describe, expect, it, vi, beforeEach } from 'vitest'; import { handler_test } from '@/pages/api/core/evaluation/dataset/data/update'; import { authUserPer } from '@fastgpt/service/support/permission/user/auth'; +import { authEvalDataset } from '@fastgpt/service/support/permission/evaluation/auth'; +import { authEvaluationDatasetDataUpdateById } from '@fastgpt/service/core/evaluation/common'; import { mongoSessionRun } from '@fastgpt/service/common/mongo/sessionRun'; import { MongoEvalDatasetData } from '@fastgpt/service/core/evaluation/dataset/evalDatasetDataSchema'; import { MongoEvalDatasetCollection } from '@fastgpt/service/core/evaluation/dataset/evalDatasetCollectionSchema'; @@ -13,17 +15,30 @@ import { import { addLog } from '@fastgpt/service/common/system/log'; vi.mock('@fastgpt/service/support/permission/user/auth'); +// Mock the evaluation permissions +vi.mock('@fastgpt/service/support/permission/evaluation/auth', () => ({ + authEvalDataset: vi.fn() +})); +vi.mock('@fastgpt/service/core/evaluation/common', () => ({ + authEvaluationDatasetDataUpdateById: vi.fn() +})); vi.mock('@fastgpt/service/common/mongo/sessionRun'); vi.mock('@fastgpt/service/core/evaluation/dataset/evalDatasetDataSchema', () => ({ MongoEvalDatasetData: { - findById: vi.fn(), + findById: vi.fn(() => ({ + select: vi.fn(() => ({ + lean: vi.fn() + })), + session: vi.fn() + })), updateOne: vi.fn() } })); vi.mock('@fastgpt/service/core/evaluation/dataset/evalDatasetCollectionSchema', () => ({ MongoEvalDatasetCollection: { findOne: vi.fn() - } + }, + EvalDatasetCollectionName: 'eval_dataset_collections' })); vi.mock('@fastgpt/service/core/evaluation/dataset/dataQualityMq', () => ({ removeEvalDatasetDataQualityJobsRobust: vi.fn(), @@ -40,6 +55,8 @@ vi.mock('@fastgpt/service/support/user/audit/util', () => ({ })); const mockAuthUserPer = vi.mocked(authUserPer); +const mockAuthEvalDataset = vi.mocked(authEvalDataset); +const mockAuthEvaluationDatasetDataUpdateById = vi.mocked(authEvaluationDatasetDataUpdateById); const mockMongoSessionRun = vi.mocked(mongoSessionRun); const mockMongoEvalDatasetData = vi.mocked(MongoEvalDatasetData); const mockMongoEvalDatasetCollection = vi.mocked(MongoEvalDatasetCollection); @@ -58,11 +75,24 @@ describe('EvalDatasetData Update API', () => { beforeEach(() => { vi.clearAllMocks(); + mockAuthEvaluationDatasetDataUpdateById.mockResolvedValue({ + teamId: validTeamId, + tmbId: validTmbId, + collectionId: validCollectionId + }); + mockAuthUserPer.mockResolvedValue({ teamId: validTeamId, tmbId: validTmbId }); + mockAuthEvalDataset.mockResolvedValue({ + teamId: validTeamId, + tmbId: validTmbId, + dataset: { _id: validCollectionId, teamId: validTeamId }, + isOwner: true + } as any); + const mockExistingData = { _id: validDataId, datasetId: validCollectionId, @@ -70,9 +100,15 @@ describe('EvalDatasetData Update API', () => { [EvalDatasetDataKeyEnum.ExpectedOutput]: 'Old output' }; - mockMongoEvalDatasetData.findById.mockReturnValue({ + // Mock for authentication pattern: findById().select().lean() + const mockSelectChain = { + lean: vi.fn().mockResolvedValue({ datasetId: validCollectionId }) + }; + const mockFindByIdChain = { + select: vi.fn(() => mockSelectChain), session: vi.fn().mockResolvedValue(mockExistingData) - } as any); + }; + mockMongoEvalDatasetData.findById.mockReturnValue(mockFindByIdChain as any); mockMongoEvalDatasetCollection.findOne.mockReturnValue({ session: vi.fn().mockResolvedValue({ @@ -381,7 +417,7 @@ describe('EvalDatasetData Update API', () => { }); describe('Authentication and Authorization', () => { - it('should call authUserPer with correct parameters', async () => { + it('should call authEvaluationDatasetDataUpdateById with correct parameters', async () => { const req = { body: { dataId: validDataId, @@ -393,17 +429,16 @@ describe('EvalDatasetData Update API', () => { await handler_test(req as any); - expect(mockAuthUserPer).toHaveBeenCalledWith({ + expect(mockAuthEvaluationDatasetDataUpdateById).toHaveBeenCalledWith(validDataId, { req, authToken: true, - authApiKey: true, - per: WritePermissionVal + authApiKey: true }); }); it('should propagate authentication errors', async () => { const authError = new Error('Authentication failed'); - mockAuthUserPer.mockRejectedValue(authError); + mockAuthEvaluationDatasetDataUpdateById.mockRejectedValue(authError); const req = { body: { @@ -420,9 +455,8 @@ describe('EvalDatasetData Update API', () => { describe('Data Validation', () => { it('should reject when dataset data does not exist', async () => { - mockMongoEvalDatasetData.findById.mockReturnValue({ - session: vi.fn().mockResolvedValue(null) - } as any); + const dataNotFoundError = new Error('Dataset data not found'); + mockAuthEvaluationDatasetDataUpdateById.mockRejectedValue(dataNotFoundError); const req = { body: { @@ -433,13 +467,12 @@ describe('EvalDatasetData Update API', () => { } }; - await expect(handler_test(req as any)).rejects.toEqual('Dataset data not found'); + await expect(handler_test(req as any)).rejects.toBe(dataNotFoundError); }); it('should reject when collection does not exist', async () => { - mockMongoEvalDatasetCollection.findOne.mockReturnValue({ - session: vi.fn().mockResolvedValue(null) - } as any); + const collectionNotFoundError = new Error('Access denied or dataset collection not found'); + mockAuthEvaluationDatasetDataUpdateById.mockRejectedValue(collectionNotFoundError); const req = { body: { @@ -450,15 +483,12 @@ describe('EvalDatasetData Update API', () => { } }; - await expect(handler_test(req as any)).rejects.toEqual( - 'Access denied or dataset collection not found' - ); + await expect(handler_test(req as any)).rejects.toBe(collectionNotFoundError); }); it('should reject when collection belongs to different team', async () => { - mockMongoEvalDatasetCollection.findOne.mockReturnValue({ - session: vi.fn().mockResolvedValue(null) - } as any); + const accessDeniedError = new Error('Access denied or dataset collection not found'); + mockAuthEvaluationDatasetDataUpdateById.mockRejectedValue(accessDeniedError); const req = { body: { @@ -469,9 +499,7 @@ describe('EvalDatasetData Update API', () => { } }; - await expect(handler_test(req as any)).rejects.toEqual( - 'Access denied or dataset collection not found' - ); + await expect(handler_test(req as any)).rejects.toBe(accessDeniedError); }); }); @@ -903,12 +931,12 @@ describe('EvalDatasetData Update API', () => { it('should handle MongoDB ObjectId-like strings for dataId', async () => { const objectIdLikeDataId = '507f1f77bcf86cd799439011'; - mockMongoEvalDatasetData.findById.mockReturnValue({ - session: vi.fn().mockResolvedValue({ - _id: objectIdLikeDataId, - datasetId: validCollectionId - }) - } as any); + // Reset auth mock to work with new dataId + mockAuthEvaluationDatasetDataUpdateById.mockResolvedValue({ + teamId: validTeamId, + tmbId: validTmbId, + collectionId: validCollectionId + }); const req = { body: { @@ -921,6 +949,11 @@ describe('EvalDatasetData Update API', () => { const result = await handler_test(req as any); expect(result).toBe('success'); + expect(mockAuthEvaluationDatasetDataUpdateById).toHaveBeenCalledWith(objectIdLikeDataId, { + req, + authToken: true, + authApiKey: true + }); }); it('should handle quality evaluation with different models', async () => { diff --git a/test/cases/pages/api/core/evaluation/metric/create.test.ts b/test/cases/pages/api/core/evaluation/metric/create.test.ts index ccca45dfd119..dd576a50a94b 100644 --- a/test/cases/pages/api/core/evaluation/metric/create.test.ts +++ b/test/cases/pages/api/core/evaluation/metric/create.test.ts @@ -2,7 +2,7 @@ import { describe, expect, it, vi, beforeEach, afterEach } from 'vitest'; import { handler } from '@/pages/api/core/evaluation/metric/create'; import { MongoEvalMetric } from '@fastgpt/service/core/evaluation/metric/schema'; import { authUserPer } from '@fastgpt/service/support/permission/user/auth'; -import { EvalMetricTypeEnum } from '@fastgpt/global/core/evaluation/constants'; +import { EvalMetricTypeEnum } from '@fastgpt/global/core/evaluation/metric/constants'; import type { CreateMetricBody } from '@fastgpt/global/core/evaluation/metric/api'; // Mock dependencies @@ -58,7 +58,17 @@ describe('/api/core/evaluation/metric/create', () => { name: 'Test Metric', description: 'Test Description', prompt: 'Test prompt for evaluation' - } as CreateMetricBody + } as CreateMetricBody, + auth: { + userId: '507f1f77bcf86cd799439013', + teamId: mockTeamId, + tmbId: mockTmbId, + appId: '', + authType: 'token' as any, + sourceName: undefined, + apikey: '', + isRoot: false + } }; const result = await handler(req as any, {} as any); @@ -118,7 +128,17 @@ describe('/api/core/evaluation/metric/create', () => { body: { name: 'Test Metric', prompt: 'Test prompt for evaluation' - } as CreateMetricBody + } as CreateMetricBody, + auth: { + userId: '507f1f77bcf86cd799439013', + teamId: mockTeamId, + tmbId: mockTmbId, + appId: '', + authType: 'token' as any, + sourceName: undefined, + apikey: '', + isRoot: false + } }; const result = await handler(req as any, {} as any); @@ -145,65 +165,145 @@ describe('/api/core/evaluation/metric/create', () => { }); it('should reject when name is missing', async () => { + // Mock auth response (auth happens before validation) + vi.mocked(authUserPer).mockResolvedValue({ + userId: '507f1f77bcf86cd799439013', + teamId: mockTeamId, + tmbId: mockTmbId, + isRoot: false, + permission: {} as any, + tmb: {} as any + }); + const req = { body: { prompt: 'Test prompt for evaluation' - } as CreateMetricBody + } as CreateMetricBody, + auth: { + userId: '507f1f77bcf86cd799439013', + teamId: mockTeamId, + tmbId: mockTmbId, + appId: '', + authType: 'token' as any, + sourceName: undefined, + apikey: '', + isRoot: false + } }; await expect(handler(req as any, {} as any)).rejects.toBe( 'Metric name is required and must be a non-empty string' ); - // Verify no auth or database calls were made - expect(authUserPer).not.toHaveBeenCalled(); + // Auth should be called but database should not + expect(authUserPer).toHaveBeenCalled(); expect(MongoEvalMetric.create).not.toHaveBeenCalled(); }); it('should reject when name is empty string', async () => { + // Mock auth response (auth happens before validation) + vi.mocked(authUserPer).mockResolvedValue({ + userId: '507f1f77bcf86cd799439013', + teamId: mockTeamId, + tmbId: mockTmbId, + isRoot: false, + permission: {} as any, + tmb: {} as any + }); + const req = { body: { name: '', prompt: 'Test prompt for evaluation' - } as CreateMetricBody + } as CreateMetricBody, + auth: { + userId: '507f1f77bcf86cd799439013', + teamId: mockTeamId, + tmbId: mockTmbId, + appId: '', + authType: 'token' as any, + sourceName: undefined, + apikey: '', + isRoot: false + } }; await expect(handler(req as any, {} as any)).rejects.toBe( 'Metric name is required and must be a non-empty string' ); - expect(authUserPer).not.toHaveBeenCalled(); + expect(authUserPer).toHaveBeenCalled(); expect(MongoEvalMetric.create).not.toHaveBeenCalled(); }); it('should reject when prompt is missing', async () => { + // Mock auth response (auth happens before validation) + vi.mocked(authUserPer).mockResolvedValue({ + userId: '507f1f77bcf86cd799439013', + teamId: mockTeamId, + tmbId: mockTmbId, + isRoot: false, + permission: {} as any, + tmb: {} as any + }); + const req = { body: { name: 'Test Metric' - } as CreateMetricBody + } as CreateMetricBody, + auth: { + userId: '507f1f77bcf86cd799439013', + teamId: mockTeamId, + tmbId: mockTmbId, + appId: '', + authType: 'token' as any, + sourceName: undefined, + apikey: '', + isRoot: false + } }; await expect(handler(req as any, {} as any)).rejects.toBe( 'Metric prompt is required and must be a non-empty string' ); - expect(authUserPer).not.toHaveBeenCalled(); + expect(authUserPer).toHaveBeenCalled(); expect(MongoEvalMetric.create).not.toHaveBeenCalled(); }); it('should reject when prompt is empty string', async () => { + // Mock auth response (auth happens before validation) + vi.mocked(authUserPer).mockResolvedValue({ + userId: '507f1f77bcf86cd799439013', + teamId: mockTeamId, + tmbId: mockTmbId, + isRoot: false, + permission: {} as any, + tmb: {} as any + }); + const req = { body: { name: 'Test Metric', prompt: '' - } as CreateMetricBody + } as CreateMetricBody, + auth: { + userId: '507f1f77bcf86cd799439013', + teamId: mockTeamId, + tmbId: mockTmbId, + appId: '', + authType: 'token' as any, + sourceName: undefined, + apikey: '', + isRoot: false + } }; await expect(handler(req as any, {} as any)).rejects.toBe( 'Metric prompt is required and must be a non-empty string' ); - expect(authUserPer).not.toHaveBeenCalled(); + expect(authUserPer).toHaveBeenCalled(); expect(MongoEvalMetric.create).not.toHaveBeenCalled(); }); @@ -215,7 +315,17 @@ describe('/api/core/evaluation/metric/create', () => { body: { name: 'Test Metric', prompt: 'Test prompt for evaluation' - } as CreateMetricBody + } as CreateMetricBody, + auth: { + userId: '507f1f77bcf86cd799439013', + teamId: mockTeamId, + tmbId: mockTmbId, + appId: '', + authType: 'token' as any, + sourceName: undefined, + apikey: '', + isRoot: false + } }; await expect(handler(req as any, {} as any)).rejects.toThrow('Authentication failed'); @@ -241,7 +351,17 @@ describe('/api/core/evaluation/metric/create', () => { body: { name: 'Test Metric', prompt: 'Test prompt for evaluation' - } as CreateMetricBody + } as CreateMetricBody, + auth: { + userId: '507f1f77bcf86cd799439013', + teamId: mockTeamId, + tmbId: mockTmbId, + appId: '', + authType: 'token' as any, + sourceName: undefined, + apikey: '', + isRoot: false + } }; await expect(handler(req as any, {} as any)).rejects.toThrow('Database creation failed'); diff --git a/test/cases/pages/api/core/evaluation/metric/delete.test.ts b/test/cases/pages/api/core/evaluation/metric/delete.test.ts index df47c0bee510..6a609e68551b 100644 --- a/test/cases/pages/api/core/evaluation/metric/delete.test.ts +++ b/test/cases/pages/api/core/evaluation/metric/delete.test.ts @@ -1,8 +1,8 @@ import { describe, expect, it, vi, beforeEach } from 'vitest'; import { handler } from '@/pages/api/core/evaluation/metric/delete'; import { MongoEvalMetric } from '@fastgpt/service/core/evaluation/metric/schema'; -import { authUserPer } from '@fastgpt/service/support/permission/user/auth'; -import { EvalMetricTypeEnum } from '@fastgpt/global/core/evaluation/constants'; +import { authEvalMetric } from '@fastgpt/service/support/permission/evaluation/auth'; +import { EvalMetricTypeEnum } from '@fastgpt/global/core/evaluation/metric/constants'; // Mock dependencies vi.mock('@fastgpt/service/core/evaluation/metric/schema', () => ({ @@ -12,8 +12,8 @@ vi.mock('@fastgpt/service/core/evaluation/metric/schema', () => ({ } })); -vi.mock('@fastgpt/service/support/permission/user/auth', () => ({ - authUserPer: vi.fn() +vi.mock('@fastgpt/service/support/permission/evaluation/auth', () => ({ + authEvalMetric: vi.fn() })); describe('/api/core/evaluation/metric/delete', () => { @@ -26,14 +26,14 @@ describe('/api/core/evaluation/metric/delete', () => { }); it('should delete a custom metric successfully', async () => { - // Mock auth response - vi.mocked(authUserPer).mockResolvedValue({ + // Mock auth response for the new auth function + vi.mocked(authEvalMetric).mockResolvedValue({ userId: '507f1f77bcf86cd799439013', teamId: mockTeamId, tmbId: mockTmbId, isRoot: false, permission: {} as any, - tmb: {} as any + metric: {} as any }); // Mock metric found - custom type @@ -48,16 +48,27 @@ describe('/api/core/evaluation/metric/delete', () => { const req = { query: { id: mockMetricId + }, + auth: { + userId: '507f1f77bcf86cd799439013', + teamId: mockTeamId, + tmbId: mockTmbId, + appId: '', + authType: 'token' as any, + sourceName: undefined, + apikey: '', + isRoot: false } }; const result = await handler(req as any, {} as any); // Verify auth was called correctly - expect(authUserPer).toHaveBeenCalledWith({ + expect(authEvalMetric).toHaveBeenCalledWith({ req, authToken: true, authApiKey: true, + metricId: mockMetricId, per: expect.any(Number) }); @@ -79,7 +90,7 @@ describe('/api/core/evaluation/metric/delete', () => { await expect(handler(req as any, {} as any)).rejects.toBe('Missing required parameter: id'); // Verify no auth or database calls were made - expect(authUserPer).not.toHaveBeenCalled(); + expect(authEvalMetric).not.toHaveBeenCalled(); expect(MongoEvalMetric.findById).not.toHaveBeenCalled(); expect(MongoEvalMetric.findByIdAndDelete).not.toHaveBeenCalled(); }); @@ -93,20 +104,20 @@ describe('/api/core/evaluation/metric/delete', () => { await expect(handler(req as any, {} as any)).rejects.toBe('Missing required parameter: id'); - expect(authUserPer).not.toHaveBeenCalled(); + expect(authEvalMetric).not.toHaveBeenCalled(); expect(MongoEvalMetric.findById).not.toHaveBeenCalled(); expect(MongoEvalMetric.findByIdAndDelete).not.toHaveBeenCalled(); }); it('should reject when metric is not found', async () => { // Mock auth response - vi.mocked(authUserPer).mockResolvedValue({ + vi.mocked(authEvalMetric).mockResolvedValue({ userId: '507f1f77bcf86cd799439013', teamId: mockTeamId, tmbId: mockTmbId, isRoot: false, permission: {} as any, - tmb: {} as any + metric: {} as any }); // Mock metric not found @@ -115,25 +126,35 @@ describe('/api/core/evaluation/metric/delete', () => { const req = { query: { id: mockMetricId + }, + auth: { + userId: '507f1f77bcf86cd799439013', + teamId: mockTeamId, + tmbId: mockTmbId, + appId: '', + authType: 'token' as any, + sourceName: undefined, + apikey: '', + isRoot: false } }; await expect(handler(req as any, {} as any)).rejects.toBe('Metric not found'); - expect(authUserPer).toHaveBeenCalled(); + expect(authEvalMetric).toHaveBeenCalled(); expect(MongoEvalMetric.findById).toHaveBeenCalledWith(mockMetricId); expect(MongoEvalMetric.findByIdAndDelete).not.toHaveBeenCalled(); }); it('should reject when trying to delete builtin metric', async () => { // Mock auth response - vi.mocked(authUserPer).mockResolvedValue({ + vi.mocked(authEvalMetric).mockResolvedValue({ userId: '507f1f77bcf86cd799439013', teamId: mockTeamId, tmbId: mockTmbId, isRoot: false, permission: {} as any, - tmb: {} as any + metric: {} as any }); // Mock metric found - builtin type @@ -147,41 +168,61 @@ describe('/api/core/evaluation/metric/delete', () => { const req = { query: { id: mockMetricId + }, + auth: { + userId: '507f1f77bcf86cd799439013', + teamId: mockTeamId, + tmbId: mockTmbId, + appId: '', + authType: 'token' as any, + sourceName: undefined, + apikey: '', + isRoot: false } }; await expect(handler(req as any, {} as any)).rejects.toBe('Builtin metrics cannot be deleted'); - expect(authUserPer).toHaveBeenCalled(); + expect(authEvalMetric).toHaveBeenCalled(); expect(MongoEvalMetric.findById).toHaveBeenCalledWith(mockMetricId); expect(MongoEvalMetric.findByIdAndDelete).not.toHaveBeenCalled(); }); it('should handle auth failure', async () => { const authError = new Error('Authentication failed'); - vi.mocked(authUserPer).mockRejectedValue(authError); + vi.mocked(authEvalMetric).mockRejectedValue(authError); const req = { query: { id: mockMetricId + }, + auth: { + userId: '507f1f77bcf86cd799439013', + teamId: mockTeamId, + tmbId: mockTmbId, + appId: '', + authType: 'token' as any, + sourceName: undefined, + apikey: '', + isRoot: false } }; await expect(handler(req as any, {} as any)).rejects.toThrow('Authentication failed'); - expect(authUserPer).toHaveBeenCalled(); + expect(authEvalMetric).toHaveBeenCalled(); expect(MongoEvalMetric.findById).not.toHaveBeenCalled(); expect(MongoEvalMetric.findByIdAndDelete).not.toHaveBeenCalled(); }); it('should handle database findById failure', async () => { - vi.mocked(authUserPer).mockResolvedValue({ + vi.mocked(authEvalMetric).mockResolvedValue({ userId: '507f1f77bcf86cd799439013', teamId: mockTeamId, tmbId: mockTmbId, isRoot: false, permission: {} as any, - tmb: {} as any + metric: {} as any }); const dbError = new Error('Database query failed'); @@ -190,24 +231,34 @@ describe('/api/core/evaluation/metric/delete', () => { const req = { query: { id: mockMetricId + }, + auth: { + userId: '507f1f77bcf86cd799439013', + teamId: mockTeamId, + tmbId: mockTmbId, + appId: '', + authType: 'token' as any, + sourceName: undefined, + apikey: '', + isRoot: false } }; await expect(handler(req as any, {} as any)).rejects.toThrow('Database query failed'); - expect(authUserPer).toHaveBeenCalled(); + expect(authEvalMetric).toHaveBeenCalled(); expect(MongoEvalMetric.findById).toHaveBeenCalledWith(mockMetricId); expect(MongoEvalMetric.findByIdAndDelete).not.toHaveBeenCalled(); }); it('should handle database deletion failure', async () => { - vi.mocked(authUserPer).mockResolvedValue({ + vi.mocked(authEvalMetric).mockResolvedValue({ userId: '507f1f77bcf86cd799439013', teamId: mockTeamId, tmbId: mockTmbId, isRoot: false, permission: {} as any, - tmb: {} as any + metric: {} as any }); const mockMetric = { @@ -223,12 +274,22 @@ describe('/api/core/evaluation/metric/delete', () => { const req = { query: { id: mockMetricId + }, + auth: { + userId: '507f1f77bcf86cd799439013', + teamId: mockTeamId, + tmbId: mockTmbId, + appId: '', + authType: 'token' as any, + sourceName: undefined, + apikey: '', + isRoot: false } }; await expect(handler(req as any, {} as any)).rejects.toThrow('Database deletion failed'); - expect(authUserPer).toHaveBeenCalled(); + expect(authEvalMetric).toHaveBeenCalled(); expect(MongoEvalMetric.findById).toHaveBeenCalledWith(mockMetricId); expect(MongoEvalMetric.findByIdAndDelete).toHaveBeenCalledWith(mockMetricId); }); diff --git a/test/cases/pages/api/core/evaluation/metric/detail.test.ts b/test/cases/pages/api/core/evaluation/metric/detail.test.ts index 46341d2773d1..24d62287f79b 100644 --- a/test/cases/pages/api/core/evaluation/metric/detail.test.ts +++ b/test/cases/pages/api/core/evaluation/metric/detail.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it, vi, beforeEach } from 'vitest'; import { handler } from '@/pages/api/core/evaluation/metric/detail'; import { MongoEvalMetric } from '@fastgpt/service/core/evaluation/metric/schema'; -import { authUserPer } from '@fastgpt/service/support/permission/user/auth'; +import { authEvalMetric } from '@fastgpt/service/support/permission/evaluation/auth'; // Mock dependencies vi.mock('@fastgpt/service/core/evaluation/metric/schema', () => ({ @@ -12,8 +12,8 @@ vi.mock('@fastgpt/service/core/evaluation/metric/schema', () => ({ } })); -vi.mock('@fastgpt/service/support/permission/user/auth', () => ({ - authUserPer: vi.fn() +vi.mock('@fastgpt/service/support/permission/evaluation/auth', () => ({ + authEvalMetric: vi.fn() })); describe('/api/core/evaluation/metric/detail', () => { @@ -28,13 +28,13 @@ describe('/api/core/evaluation/metric/detail', () => { it('should return metric details successfully', async () => { // Mock auth response - vi.mocked(authUserPer).mockResolvedValue({ + vi.mocked(authEvalMetric).mockResolvedValue({ userId: mockUserId, teamId: mockTeamId, tmbId: mockTmbId, isRoot: false, permission: {} as any, - tmb: {} as any + metric: {} as any }); // Mock metric data @@ -59,16 +59,27 @@ describe('/api/core/evaluation/metric/detail', () => { const req = { query: { id: mockMetricId + }, + auth: { + userId: mockUserId, + teamId: mockTeamId, + tmbId: mockTmbId, + appId: '', + authType: 'token' as any, + sourceName: undefined, + apikey: '', + isRoot: false } }; const result = await handler(req as any, {} as any); // Verify auth was called correctly - expect(authUserPer).toHaveBeenCalledWith({ + expect(authEvalMetric).toHaveBeenCalledWith({ req, authToken: true, authApiKey: true, + metricId: mockMetricId, per: expect.any(Number) }); @@ -81,39 +92,49 @@ describe('/api/core/evaluation/metric/detail', () => { }); it('should reject when id parameter is missing', async () => { + // Mock auth to fail due to missing id + vi.mocked(authEvalMetric).mockRejectedValue(new Error('Evaluation metric ID is required')); + const req = { query: {} }; - await expect(handler(req as any, {} as any)).rejects.toBe('Missing required parameter: id'); + await expect(handler(req as any, {} as any)).rejects.toThrow( + 'Evaluation metric ID is required' + ); - // Verify no auth or database calls were made - expect(authUserPer).not.toHaveBeenCalled(); + // Verify auth was called but database was not + expect(authEvalMetric).toHaveBeenCalled(); expect(MongoEvalMetric.findById).not.toHaveBeenCalled(); }); it('should reject when id parameter is empty string', async () => { + // Mock auth to fail due to empty id + vi.mocked(authEvalMetric).mockRejectedValue(new Error('Evaluation metric ID is required')); + const req = { query: { id: '' } }; - await expect(handler(req as any, {} as any)).rejects.toBe('Missing required parameter: id'); + await expect(handler(req as any, {} as any)).rejects.toThrow( + 'Evaluation metric ID is required' + ); - expect(authUserPer).not.toHaveBeenCalled(); + expect(authEvalMetric).toHaveBeenCalled(); expect(MongoEvalMetric.findById).not.toHaveBeenCalled(); }); it('should reject when metric is not found', async () => { // Mock auth response - vi.mocked(authUserPer).mockResolvedValue({ + vi.mocked(authEvalMetric).mockResolvedValue({ userId: mockUserId, teamId: mockTeamId, tmbId: mockTmbId, isRoot: false, permission: {} as any, - tmb: {} as any + metric: {} as any }); // Mock metric not found @@ -130,14 +151,14 @@ describe('/api/core/evaluation/metric/detail', () => { await expect(handler(req as any, {} as any)).rejects.toBe('Metric not found'); - expect(authUserPer).toHaveBeenCalled(); + expect(authEvalMetric).toHaveBeenCalled(); expect(MongoEvalMetric.findById).toHaveBeenCalledWith(mockMetricId); expect(mockQuery.lean).toHaveBeenCalled(); }); it('should handle auth failure', async () => { const authError = new Error('Authentication failed'); - vi.mocked(authUserPer).mockRejectedValue(authError); + vi.mocked(authEvalMetric).mockRejectedValue(authError); const req = { query: { @@ -147,18 +168,18 @@ describe('/api/core/evaluation/metric/detail', () => { await expect(handler(req as any, {} as any)).rejects.toThrow('Authentication failed'); - expect(authUserPer).toHaveBeenCalled(); + expect(authEvalMetric).toHaveBeenCalled(); expect(MongoEvalMetric.findById).not.toHaveBeenCalled(); }); it('should handle database query failure', async () => { - vi.mocked(authUserPer).mockResolvedValue({ + vi.mocked(authEvalMetric).mockResolvedValue({ userId: mockUserId, teamId: mockTeamId, tmbId: mockTmbId, isRoot: false, permission: {} as any, - tmb: {} as any + metric: {} as any }); const dbError = new Error('Database query failed'); @@ -175,20 +196,20 @@ describe('/api/core/evaluation/metric/detail', () => { await expect(handler(req as any, {} as any)).rejects.toThrow('Database query failed'); - expect(authUserPer).toHaveBeenCalled(); + expect(authEvalMetric).toHaveBeenCalled(); expect(MongoEvalMetric.findById).toHaveBeenCalledWith(mockMetricId); expect(mockQuery.lean).toHaveBeenCalled(); }); it('should return metric with all fields when found', async () => { // Mock auth response - vi.mocked(authUserPer).mockResolvedValue({ + vi.mocked(authEvalMetric).mockResolvedValue({ userId: mockUserId, teamId: mockTeamId, tmbId: mockTmbId, isRoot: false, permission: {} as any, - tmb: {} as any + metric: {} as any }); // Mock comprehensive metric data diff --git a/test/cases/pages/api/core/evaluation/metric/list.test.ts b/test/cases/pages/api/core/evaluation/metric/list.test.ts index a27ac6979a8a..9a44f5bd1d71 100644 --- a/test/cases/pages/api/core/evaluation/metric/list.test.ts +++ b/test/cases/pages/api/core/evaluation/metric/list.test.ts @@ -3,6 +3,7 @@ import { handler } from '@/pages/api/core/evaluation/metric/list'; import { MongoEvalMetric } from '@fastgpt/service/core/evaluation/metric/schema'; import { authUserPer } from '@fastgpt/service/support/permission/user/auth'; import { addSourceMember } from '@fastgpt/service/support/user/utils'; +import { getEvaluationPermissionAggregation } from '@fastgpt/service/core/evaluation/common'; import type { ListMetricsBody } from '@fastgpt/global/core/evaluation/metric/api'; import { Types } from '@fastgpt/service/common/mongo'; @@ -30,6 +31,10 @@ vi.mock('@fastgpt/service/support/user/utils', () => ({ addSourceMember: vi.fn() })); +vi.mock('@fastgpt/service/core/evaluation/common', () => ({ + getEvaluationPermissionAggregation: vi.fn() +})); + describe('/api/core/evaluation/metric/list', () => { const mockTeamId = '507f1f77bcf86cd799439011'; const mockTmbId = '507f1f77bcf86cd799439012'; @@ -50,6 +55,16 @@ describe('/api/core/evaluation/metric/list', () => { tmb: {} as any }); + // Mock permission aggregation response + vi.mocked(getEvaluationPermissionAggregation).mockResolvedValue({ + teamId: mockTeamId, + tmbId: mockTmbId, + isOwner: false, + roleList: [], + myGroupMap: new Map(), + myOrgSet: new Set() + }); + // Mock database response const mockMetrics = [ { @@ -58,7 +73,8 @@ describe('/api/core/evaluation/metric/list', () => { description: 'Description 1', createTime: new Date('2024-01-01'), updateTime: new Date('2024-01-01'), - tmbId: mockTmbId + tmbId: mockTmbId, + permission: { hasReadPer: true } }, { _id: 'metric2', @@ -66,7 +82,8 @@ describe('/api/core/evaluation/metric/list', () => { description: 'Description 2', createTime: new Date('2024-01-02'), updateTime: new Date('2024-01-02'), - tmbId: mockTmbId + tmbId: mockTmbId, + permission: { hasReadPer: true } } ]; @@ -112,7 +129,17 @@ describe('/api/core/evaluation/metric/list', () => { body: { pageNum: 1, pageSize: 10 - } as ListMetricsBody + } as ListMetricsBody, + auth: { + userId: mockUserId, + teamId: mockTeamId, + tmbId: mockTmbId, + appId: '', + authType: 'token' as any, + sourceName: undefined, + apikey: '', + isRoot: false + } }; const result = await handler(req as any); @@ -125,8 +152,25 @@ describe('/api/core/evaluation/metric/list', () => { per: expect.any(Number) }); - // Verify database query + // Verify permission aggregation was called + expect(getEvaluationPermissionAggregation).toHaveBeenCalledWith({ + req, + authApiKey: true, + authToken: true + }); + + // Verify database query with expected filter structure expect(MongoEvalMetric.find).toHaveBeenCalledWith({ + $or: [ + { + _id: { + $in: [] + } + }, + { + tmbId: new Types.ObjectId(mockTmbId) + } + ], teamId: new Types.ObjectId(mockTeamId) }); expect(mockFind.sort).toHaveBeenCalledWith({ createTime: -1 }); @@ -135,29 +179,43 @@ describe('/api/core/evaluation/metric/list', () => { // Verify count query expect(MongoEvalMetric.countDocuments).toHaveBeenCalledWith({ + $or: [ + { + _id: { + $in: [] + } + }, + { + tmbId: new Types.ObjectId(mockTmbId) + } + ], teamId: new Types.ObjectId(mockTeamId) }); - // Verify source member addition + // Verify source member addition (with permission objects added) expect(addSourceMember).toHaveBeenCalledWith({ - list: [ - { + list: expect.arrayContaining([ + expect.objectContaining({ _id: 'metric1', name: 'Metric 1', description: 'Description 1', createTime: new Date('2024-01-01'), updateTime: new Date('2024-01-01'), - tmbId: mockTmbId - }, - { + tmbId: mockTmbId, + permission: expect.any(Object), + private: expect.any(Boolean) + }), + expect.objectContaining({ _id: 'metric2', name: 'Metric 2', description: 'Description 2', createTime: new Date('2024-01-02'), updateTime: new Date('2024-01-02'), - tmbId: mockTmbId - } - ] + tmbId: mockTmbId, + permission: expect.any(Object), + private: expect.any(Boolean) + }) + ]) }); // Verify response @@ -234,6 +292,16 @@ describe('/api/core/evaluation/metric/list', () => { tmb: {} as any }); + // Mock permission aggregation response + vi.mocked(getEvaluationPermissionAggregation).mockResolvedValue({ + teamId: mockTeamId, + tmbId: mockTmbId, + isOwner: false, + roleList: [], + myGroupMap: new Map(), + myOrgSet: new Set() + }); + const mockQuery = { lean: vi.fn().mockResolvedValue([]) }; @@ -256,13 +324,33 @@ describe('/api/core/evaluation/metric/list', () => { pageNum: 1, pageSize: 10, searchKey: ' ' // whitespace only - } as ListMetricsBody + } as ListMetricsBody, + auth: { + userId: mockUserId, + teamId: mockTeamId, + tmbId: mockTmbId, + appId: '', + authType: 'token' as any, + sourceName: undefined, + apikey: '', + isRoot: false + } }; await handler(req as any); // Verify database query without search filter (whitespace is trimmed) expect(MongoEvalMetric.find).toHaveBeenCalledWith({ + $or: [ + { + _id: { + $in: [] + } + }, + { + tmbId: new Types.ObjectId(mockTmbId) + } + ], teamId: new Types.ObjectId(mockTeamId) }); }); diff --git a/test/cases/pages/api/core/evaluation/metric/update.test.ts b/test/cases/pages/api/core/evaluation/metric/update.test.ts index c8e1f55c3588..f095be2c4213 100644 --- a/test/cases/pages/api/core/evaluation/metric/update.test.ts +++ b/test/cases/pages/api/core/evaluation/metric/update.test.ts @@ -1,8 +1,8 @@ import { describe, expect, it, vi, beforeEach } from 'vitest'; import { handler } from '@/pages/api/core/evaluation/metric/update'; import { MongoEvalMetric } from '@fastgpt/service/core/evaluation/metric/schema'; -import { authUserPer } from '@fastgpt/service/support/permission/user/auth'; -import { EvalMetricTypeEnum } from '@fastgpt/global/core/evaluation/constants'; +import { authEvalMetric } from '@fastgpt/service/support/permission/evaluation/auth'; +import { EvalMetricTypeEnum } from '@fastgpt/global/core/evaluation/metric/constants'; import type { UpdateMetricBody } from '@fastgpt/global/core/evaluation/metric/api'; // Mock dependencies @@ -12,8 +12,8 @@ vi.mock('@fastgpt/service/core/evaluation/metric/schema', () => ({ } })); -vi.mock('@fastgpt/service/support/permission/user/auth', () => ({ - authUserPer: vi.fn() +vi.mock('@fastgpt/service/support/permission/evaluation/auth', () => ({ + authEvalMetric: vi.fn() })); describe('/api/core/evaluation/metric/update', () => { @@ -28,13 +28,13 @@ describe('/api/core/evaluation/metric/update', () => { it('should update a custom metric successfully with all fields', async () => { // Mock auth response - vi.mocked(authUserPer).mockResolvedValue({ + vi.mocked(authEvalMetric).mockResolvedValue({ userId: mockUserId, teamId: mockTeamId, tmbId: mockTmbId, isRoot: false, permission: {} as any, - tmb: {} as any + metric: {} as any }); // Mock metric found - custom type @@ -62,16 +62,27 @@ describe('/api/core/evaluation/metric/update', () => { name: 'Updated Metric', description: 'Updated Description', prompt: 'Updated Prompt' - } as UpdateMetricBody + } as UpdateMetricBody, + auth: { + userId: mockUserId, + teamId: mockTeamId, + tmbId: mockTmbId, + appId: '', + authType: 'token' as any, + sourceName: undefined, + apikey: '', + isRoot: false + } }; const result = await handler(req as any, {} as any); // Verify auth was called correctly - expect(authUserPer).toHaveBeenCalledWith({ + expect(authEvalMetric).toHaveBeenCalledWith({ req, authToken: true, authApiKey: true, + metricId: mockMetricId, per: expect.any(Number) }); @@ -94,13 +105,13 @@ describe('/api/core/evaluation/metric/update', () => { it('should update a custom metric with partial fields', async () => { // Mock auth response - vi.mocked(authUserPer).mockResolvedValue({ + vi.mocked(authEvalMetric).mockResolvedValue({ userId: mockUserId, teamId: mockTeamId, tmbId: mockTmbId, isRoot: false, permission: {} as any, - tmb: {} as any + metric: {} as any }); // Mock metric found - custom type @@ -125,7 +136,17 @@ describe('/api/core/evaluation/metric/update', () => { id: mockMetricId, name: 'Updated Metric Only' // description and prompt not provided - } as UpdateMetricBody + } as UpdateMetricBody, + auth: { + userId: mockUserId, + teamId: mockTeamId, + tmbId: mockTmbId, + appId: '', + authType: 'token' as any, + sourceName: undefined, + apikey: '', + isRoot: false + } }; const result = await handler(req as any, {} as any); @@ -145,42 +166,72 @@ describe('/api/core/evaluation/metric/update', () => { }); it('should reject when id is missing', async () => { + // Mock auth to fail due to missing id + vi.mocked(authEvalMetric).mockRejectedValue(new Error('Evaluation metric ID is required')); + const req = { body: { name: 'Test Metric' - } as UpdateMetricBody + } as UpdateMetricBody, + auth: { + userId: mockUserId, + teamId: mockTeamId, + tmbId: mockTmbId, + appId: '', + authType: 'token' as any, + sourceName: undefined, + apikey: '', + isRoot: false + } }; - await expect(handler(req as any, {} as any)).rejects.toBe('Missing required parameter: id'); + await expect(handler(req as any, {} as any)).rejects.toThrow( + 'Evaluation metric ID is required' + ); - // Verify no auth or database calls were made - expect(authUserPer).not.toHaveBeenCalled(); + // Auth should be called but database should not + expect(authEvalMetric).toHaveBeenCalled(); expect(MongoEvalMetric.findById).not.toHaveBeenCalled(); }); it('should reject when id is empty string', async () => { + // Mock auth to fail due to empty id + vi.mocked(authEvalMetric).mockRejectedValue(new Error('Evaluation metric ID is required')); + const req = { body: { id: '', name: 'Test Metric' - } as UpdateMetricBody + } as UpdateMetricBody, + auth: { + userId: mockUserId, + teamId: mockTeamId, + tmbId: mockTmbId, + appId: '', + authType: 'token' as any, + sourceName: undefined, + apikey: '', + isRoot: false + } }; - await expect(handler(req as any, {} as any)).rejects.toBe('Missing required parameter: id'); + await expect(handler(req as any, {} as any)).rejects.toThrow( + 'Evaluation metric ID is required' + ); - expect(authUserPer).not.toHaveBeenCalled(); + expect(authEvalMetric).toHaveBeenCalled(); expect(MongoEvalMetric.findById).not.toHaveBeenCalled(); }); it('should reject when metric is not found', async () => { // Mock auth response - vi.mocked(authUserPer).mockResolvedValue({ + vi.mocked(authEvalMetric).mockResolvedValue({ userId: mockUserId, teamId: mockTeamId, tmbId: mockTmbId, isRoot: false, permission: {} as any, - tmb: {} as any + metric: {} as any }); // Mock metric not found @@ -190,24 +241,34 @@ describe('/api/core/evaluation/metric/update', () => { body: { id: mockMetricId, name: 'Test Metric' - } as UpdateMetricBody + } as UpdateMetricBody, + auth: { + userId: mockUserId, + teamId: mockTeamId, + tmbId: mockTmbId, + appId: '', + authType: 'token' as any, + sourceName: undefined, + apikey: '', + isRoot: false + } }; await expect(handler(req as any, {} as any)).rejects.toBe('Metric not found'); - expect(authUserPer).toHaveBeenCalled(); + expect(authEvalMetric).toHaveBeenCalled(); expect(MongoEvalMetric.findById).toHaveBeenCalledWith(mockMetricId); }); it('should reject when trying to update builtin metric', async () => { // Mock auth response - vi.mocked(authUserPer).mockResolvedValue({ + vi.mocked(authEvalMetric).mockResolvedValue({ userId: mockUserId, teamId: mockTeamId, tmbId: mockTmbId, isRoot: false, permission: {} as any, - tmb: {} as any + metric: {} as any }); // Mock metric found - builtin type @@ -223,41 +284,61 @@ describe('/api/core/evaluation/metric/update', () => { body: { id: mockMetricId, name: 'Updated Builtin Metric' - } as UpdateMetricBody + } as UpdateMetricBody, + auth: { + userId: mockUserId, + teamId: mockTeamId, + tmbId: mockTmbId, + appId: '', + authType: 'token' as any, + sourceName: undefined, + apikey: '', + isRoot: false + } }; await expect(handler(req as any, {} as any)).rejects.toBe('Builtin metric cannot be modified'); - expect(authUserPer).toHaveBeenCalled(); + expect(authEvalMetric).toHaveBeenCalled(); expect(MongoEvalMetric.findById).toHaveBeenCalledWith(mockMetricId); expect(mockMetric.save).not.toHaveBeenCalled(); }); it('should handle auth failure', async () => { const authError = new Error('Authentication failed'); - vi.mocked(authUserPer).mockRejectedValue(authError); + vi.mocked(authEvalMetric).mockRejectedValue(authError); const req = { body: { id: mockMetricId, name: 'Test Metric' - } as UpdateMetricBody + } as UpdateMetricBody, + auth: { + userId: mockUserId, + teamId: mockTeamId, + tmbId: mockTmbId, + appId: '', + authType: 'token' as any, + sourceName: undefined, + apikey: '', + isRoot: false + } }; await expect(handler(req as any, {} as any)).rejects.toThrow('Authentication failed'); - expect(authUserPer).toHaveBeenCalled(); + expect(authEvalMetric).toHaveBeenCalled(); expect(MongoEvalMetric.findById).not.toHaveBeenCalled(); }); it('should handle database findById failure', async () => { - vi.mocked(authUserPer).mockResolvedValue({ + vi.mocked(authEvalMetric).mockResolvedValue({ userId: mockUserId, teamId: mockTeamId, tmbId: mockTmbId, isRoot: false, permission: {} as any, - tmb: {} as any + metric: {} as any }); const dbError = new Error('Database query failed'); @@ -267,23 +348,33 @@ describe('/api/core/evaluation/metric/update', () => { body: { id: mockMetricId, name: 'Test Metric' - } as UpdateMetricBody + } as UpdateMetricBody, + auth: { + userId: mockUserId, + teamId: mockTeamId, + tmbId: mockTmbId, + appId: '', + authType: 'token' as any, + sourceName: undefined, + apikey: '', + isRoot: false + } }; await expect(handler(req as any, {} as any)).rejects.toThrow('Database query failed'); - expect(authUserPer).toHaveBeenCalled(); + expect(authEvalMetric).toHaveBeenCalled(); expect(MongoEvalMetric.findById).toHaveBeenCalledWith(mockMetricId); }); it('should handle database save failure', async () => { - vi.mocked(authUserPer).mockResolvedValue({ + vi.mocked(authEvalMetric).mockResolvedValue({ userId: mockUserId, teamId: mockTeamId, tmbId: mockTmbId, isRoot: false, permission: {} as any, - tmb: {} as any + metric: {} as any }); const mockMetric = { @@ -305,25 +396,35 @@ describe('/api/core/evaluation/metric/update', () => { body: { id: mockMetricId, name: 'Updated Metric' - } as UpdateMetricBody + } as UpdateMetricBody, + auth: { + userId: mockUserId, + teamId: mockTeamId, + tmbId: mockTmbId, + appId: '', + authType: 'token' as any, + sourceName: undefined, + apikey: '', + isRoot: false + } }; await expect(handler(req as any, {} as any)).rejects.toThrow('Database save failed'); - expect(authUserPer).toHaveBeenCalled(); + expect(authEvalMetric).toHaveBeenCalled(); expect(MongoEvalMetric.findById).toHaveBeenCalledWith(mockMetricId); expect(mockMetric.save).toHaveBeenCalled(); }); it('should handle empty update fields gracefully', async () => { // Mock auth response - vi.mocked(authUserPer).mockResolvedValue({ + vi.mocked(authEvalMetric).mockResolvedValue({ userId: mockUserId, teamId: mockTeamId, tmbId: mockTmbId, isRoot: false, permission: {} as any, - tmb: {} as any + metric: {} as any }); // Mock metric found - custom type @@ -344,7 +445,17 @@ describe('/api/core/evaluation/metric/update', () => { body: { id: mockMetricId // No name, description, or prompt provided - } as UpdateMetricBody + } as UpdateMetricBody, + auth: { + userId: mockUserId, + teamId: mockTeamId, + tmbId: mockTmbId, + appId: '', + authType: 'token' as any, + sourceName: undefined, + apikey: '', + isRoot: false + } }; const result = await handler(req as any, {} as any); diff --git a/test/cases/pages/api/core/evaluation/task-handler.api.test.ts b/test/cases/pages/api/core/evaluation/task-handler.api.test.ts deleted file mode 100644 index d484872fbf75..000000000000 --- a/test/cases/pages/api/core/evaluation/task-handler.api.test.ts +++ /dev/null @@ -1,480 +0,0 @@ -import { describe, test, expect, vi, beforeEach } from 'vitest'; -import { Types } from '@fastgpt/service/common/mongo'; - -// Import API handlers directly (not via HTTP wrapper) - use named imports -import { handler as createHandler } from '@/pages/api/core/evaluation/task/create'; -import { handler as listHandler } from '@/pages/api/core/evaluation/task/list'; -import { handler as detailHandler } from '@/pages/api/core/evaluation/task/detail'; -import { handler as updateHandler } from '@/pages/api/core/evaluation/task/update'; -import { handler as deleteHandler } from '@/pages/api/core/evaluation/task/delete'; -import { handler as startHandler } from '@/pages/api/core/evaluation/task/start'; -import { handler as stopHandler } from '@/pages/api/core/evaluation/task/stop'; -import { handler as statsHandler } from '@/pages/api/core/evaluation/task/stats'; - -// Mock dependencies -vi.mock('@fastgpt/service/core/evaluation/task', () => ({ - EvaluationTaskService: { - createEvaluation: vi.fn(), - listEvaluations: vi.fn(), - getEvaluation: vi.fn(), - updateEvaluation: vi.fn(), - deleteEvaluation: vi.fn(), - startEvaluation: vi.fn(), - stopEvaluation: vi.fn(), - getEvaluationStats: vi.fn() - } -})); - -vi.mock('@fastgpt/service/support/permission/teamLimit', () => ({ - checkTeamAIPoints: vi.fn() -})); - -vi.mock('@fastgpt/service/support/permission/auth/common', () => ({ - authCert: vi.fn().mockResolvedValue({ - teamId: new Types.ObjectId(), - tmbId: new Types.ObjectId() - }) -})); - -vi.mock('@fastgpt/service/core/evaluation/target', () => ({ - validateTargetConfig: vi.fn().mockResolvedValue({ - success: false, - message: 'Target validation failed' - }) -})); - -vi.mock('@fastgpt/service/common/system/log', () => ({ - addLog: { - info: vi.fn(), - error: vi.fn(), - warn: vi.fn() - } -})); - -import { EvaluationTaskService } from '@fastgpt/service/core/evaluation/task'; -import { checkTeamAIPoints } from '@fastgpt/service/support/permission/teamLimit'; -import { validateTargetConfig } from '@fastgpt/service/core/evaluation/target'; -import { addLog } from '@fastgpt/service/common/system/log'; -import { EvaluationStatusEnum } from '@fastgpt/global/core/evaluation/constants'; - -describe('Task API Handler Tests (Direct Function Calls)', () => { - const mockEvaluation = { - _id: new Types.ObjectId(), - name: 'Test Evaluation', - description: 'Test Description', - datasetId: new Types.ObjectId(), - targetId: new Types.ObjectId(), - metricIds: [new Types.ObjectId(), new Types.ObjectId()], - usageId: new Types.ObjectId(), - status: EvaluationStatusEnum.queuing, - teamId: new Types.ObjectId(), - tmbId: new Types.ObjectId(), - createTime: new Date(), - updateTime: new Date() - }; - - beforeEach(() => { - vi.clearAllMocks(); - }); - - describe('Create Evaluation Handler', () => { - test('应该成功创建评估任务', async () => { - const mockReq = { - method: 'POST', - body: { - name: 'Test Evaluation', - description: 'Test Description', - datasetId: new Types.ObjectId().toString(), - target: { - type: 'workflow', - config: { - appId: new Types.ObjectId().toString() - } - }, - evaluators: [ - { - metric: { - _id: new Types.ObjectId().toString(), - name: 'Test Metric', - type: 'ai_model', - config: { llm: 'gpt-4', prompt: 'test' }, - dependencies: ['llm'], - teamId: new Types.ObjectId().toString(), - tmbId: new Types.ObjectId().toString(), - createTime: new Date(), - updateTime: new Date() - }, - runtimeConfig: { llm: 'gpt-4' } - } - ] - } - } as any; - - (checkTeamAIPoints as any).mockResolvedValue(undefined); - (validateTargetConfig as any).mockResolvedValue({ success: true, message: 'Valid' }); - (EvaluationTaskService.createEvaluation as any).mockResolvedValue(mockEvaluation); - - const result = await createHandler(mockReq); - - expect(EvaluationTaskService.createEvaluation).toHaveBeenCalledWith( - expect.objectContaining({ - name: 'Test Evaluation', - description: 'Test Description', - datasetId: mockReq.body.datasetId, - target: mockReq.body.target, - evaluators: mockReq.body.evaluators - }), - expect.objectContaining({ - req: mockReq, - authToken: true - }) - ); - expect(result).toEqual(mockEvaluation); - expect(addLog.info).toHaveBeenCalledWith( - '[Evaluation] Evaluation task created successfully', - expect.objectContaining({ - evalId: mockEvaluation._id, - name: mockEvaluation.name - }) - ); - }); - - test('应该拒绝空名称', async () => { - const mockReq = { - method: 'POST', - body: { - name: '', - datasetId: new Types.ObjectId().toString(), - target: { - type: 'workflow', - config: { - appId: new Types.ObjectId().toString() - } - }, - evaluators: [ - { - metric: { - _id: new Types.ObjectId().toString(), - name: 'Test Metric', - type: 'ai_model', - config: { llm: 'gpt-4', prompt: 'test' }, - dependencies: ['llm'], - teamId: new Types.ObjectId().toString(), - tmbId: new Types.ObjectId().toString(), - createTime: new Date(), - updateTime: new Date() - }, - runtimeConfig: { llm: 'gpt-4' } - } - ] - } - } as any; - - await expect(createHandler(mockReq)).rejects.toMatch('Evaluation name is required'); - }); - - test('应该拒绝空指标列表', async () => { - const mockReq = { - method: 'POST', - body: { - name: 'Test Evaluation', - datasetId: new Types.ObjectId().toString(), - target: { - type: 'workflow', - config: { - appId: new Types.ObjectId().toString() - } - }, - evaluators: [] - } - } as any; - - // 由于target验证在前,空的evaluators测试会被target验证拦截,这里改为测试有效target但空evaluators的情况 - // 需要mock validateTargetConfig返回成功 - (validateTargetConfig as any).mockResolvedValue({ success: true, message: 'Valid' }); - - await expect(createHandler(mockReq)).rejects.toMatch('At least one evaluator is required'); - }); - - test('应该拒绝缺少必填字段', async () => { - const mockReq = { - method: 'POST', - body: { - name: 'Test Evaluation' - // 缺少 datasetId, target, metricIds - } - } as any; - - // 重置mock为默认的失败状态 - (validateTargetConfig as any).mockResolvedValue({ - success: false, - message: 'Target validation failed' - }); - - // 由于没有target字段,target验证会先失败 - await expect(createHandler(mockReq)).rejects.toMatch('Target validation failed'); - }); - }); - - describe('List Evaluations Handler', () => { - test('应该成功获取评估任务列表', async () => { - const mockReq = { - body: { - pageNum: 1, - pageSize: 10 - } - } as any; - - const mockResult = { - evaluations: [mockEvaluation], - total: 1 - }; - - (EvaluationTaskService.listEvaluations as any).mockResolvedValue(mockResult); - - const result = await listHandler(mockReq); - - expect(EvaluationTaskService.listEvaluations).toHaveBeenCalledWith( - expect.objectContaining({ - req: mockReq, - authToken: true - }), - 1, - 10, - undefined - ); - expect(result).toEqual({ - list: mockResult.evaluations, - total: mockResult.total - }); - }); - - test('应该处理搜索参数', async () => { - const mockReq = { - body: { - pageNum: 2, - pageSize: 20, - searchKey: 'test search' - } - } as any; - - const mockResult = { evaluations: [], total: 0 }; - (EvaluationTaskService.listEvaluations as any).mockResolvedValue(mockResult); - - await listHandler(mockReq); - - expect(EvaluationTaskService.listEvaluations).toHaveBeenCalledWith( - expect.objectContaining({ - req: mockReq, - authToken: true - }), - 2, - 20, - 'test search' - ); - }); - - test('应该使用默认分页参数', async () => { - const mockReq = { - body: {} - } as any; - - const mockResult = { evaluations: [], total: 0 }; - (EvaluationTaskService.listEvaluations as any).mockResolvedValue(mockResult); - - await listHandler(mockReq); - - expect(EvaluationTaskService.listEvaluations).toHaveBeenCalledWith( - expect.objectContaining({ - req: mockReq, - authToken: true - }), - 1, // 默认页码 - 20, // 默认页面大小 - undefined - ); - }); - }); - - describe('Get Evaluation Detail Handler', () => { - test('应该成功获取评估任务详情', async () => { - const evalId = new Types.ObjectId().toString(); - const mockReq = { - method: 'GET', - query: { evalId: evalId } - } as any; - - (EvaluationTaskService.getEvaluation as any).mockResolvedValue(mockEvaluation); - - const result = await detailHandler(mockReq); - - expect(EvaluationTaskService.getEvaluation).toHaveBeenCalledWith( - evalId, - expect.objectContaining({ - req: mockReq, - authToken: true - }) - ); - expect(result).toEqual(mockEvaluation); - }); - - test('应该拒绝缺少ID的请求', async () => { - const mockReq = { - method: 'GET', - query: {} - } as any; - - await expect(detailHandler(mockReq)).rejects.toMatch('Evaluation ID is required'); - }); - }); - - describe('Update Evaluation Handler', () => { - test('应该成功更新评估任务', async () => { - const evalId = new Types.ObjectId().toString(); - const mockReq = { - method: 'PUT', - body: { - evalId: evalId, - name: 'Updated Evaluation', - description: 'Updated Description' - } - } as any; - - (EvaluationTaskService.updateEvaluation as any).mockResolvedValue(undefined); - - const result = await updateHandler(mockReq); - - expect(EvaluationTaskService.updateEvaluation).toHaveBeenCalledWith( - evalId, - expect.objectContaining({ - name: 'Updated Evaluation', - description: 'Updated Description' - }), - expect.objectContaining({ - req: mockReq, - authToken: true - }) - ); - expect(result).toEqual({ message: 'Evaluation updated successfully' }); - }); - }); - - describe('Delete Evaluation Handler', () => { - test('应该成功删除评估任务', async () => { - const evalId = new Types.ObjectId().toString(); - const mockReq = { - method: 'DELETE', - query: { evalId: evalId } - } as any; - - (EvaluationTaskService.deleteEvaluation as any).mockResolvedValue(undefined); - - const result = await deleteHandler(mockReq); - - expect(EvaluationTaskService.deleteEvaluation).toHaveBeenCalledWith( - evalId, - expect.objectContaining({ - req: mockReq, - authToken: true - }) - ); - expect(result).toEqual({ message: 'Evaluation deleted successfully' }); - }); - }); - - describe('Start Evaluation Handler', () => { - test('应该成功启动评估任务', async () => { - const evalId = new Types.ObjectId().toString(); - const mockReq = { - method: 'POST', - body: { evalId } - } as any; - - (EvaluationTaskService.startEvaluation as any).mockResolvedValue(undefined); - - const result = await startHandler(mockReq); - - expect(EvaluationTaskService.startEvaluation).toHaveBeenCalledWith( - evalId, - expect.objectContaining({ - req: mockReq, - authToken: true - }) - ); - expect(result).toEqual({ message: 'Evaluation started successfully' }); - }); - - test('应该拒绝缺少评估ID的请求', async () => { - const mockReq = { - method: 'POST', - body: {} - } as any; - - await expect(startHandler(mockReq)).rejects.toMatch('Evaluation ID is required'); - }); - }); - - describe('Stop Evaluation Handler', () => { - test('应该成功停止评估任务', async () => { - const evalId = new Types.ObjectId().toString(); - const mockReq = { - method: 'POST', - body: { evalId } - } as any; - - (EvaluationTaskService.stopEvaluation as any).mockResolvedValue(undefined); - - const result = await stopHandler(mockReq); - - expect(EvaluationTaskService.stopEvaluation).toHaveBeenCalledWith( - evalId, - expect.objectContaining({ - req: mockReq, - authToken: true - }) - ); - expect(result).toEqual({ message: 'Evaluation stopped successfully' }); - }); - }); - - describe('Get Evaluation Stats Handler', () => { - test('应该成功获取评估任务统计信息', async () => { - const evalId = new Types.ObjectId().toString(); - const mockReq = { - method: 'GET', - query: { evalId } - } as any; - - const mockStats = { - total: 100, - completed: 80, - evaluating: 10, - queuing: 5, - error: 5, - avgScore: 85.5 - }; - - (EvaluationTaskService.getEvaluationStats as any).mockResolvedValue(mockStats); - - const result = await statsHandler(mockReq); - - expect(EvaluationTaskService.getEvaluationStats).toHaveBeenCalledWith( - evalId, - expect.objectContaining({ - req: mockReq, - authToken: true - }) - ); - expect(result).toEqual(mockStats); - }); - - test('应该拒绝缺少评估ID的请求', async () => { - const mockReq = { - method: 'GET', - query: {} - } as any; - - await expect(statsHandler(mockReq)).rejects.toMatch('Evaluation ID is required'); - }); - }); -}); diff --git a/test/cases/pages/api/core/evaluation/task/create.test.ts b/test/cases/pages/api/core/evaluation/task/create.test.ts new file mode 100644 index 000000000000..89b11a9a2f66 --- /dev/null +++ b/test/cases/pages/api/core/evaluation/task/create.test.ts @@ -0,0 +1,197 @@ +import { describe, test, expect, vi, beforeEach } from 'vitest'; +import { Types } from '@fastgpt/service/common/mongo'; +import { handler as createHandler } from '@/pages/api/core/evaluation/task/create'; +import { EvaluationTaskService } from '@fastgpt/service/core/evaluation/task'; +import { checkTeamAIPoints } from '@fastgpt/service/support/permission/teamLimit'; +import { validateTargetConfig } from '@fastgpt/service/core/evaluation/target'; +import { addLog } from '@fastgpt/service/common/system/log'; +import { EvaluationStatusEnum } from '@fastgpt/global/core/evaluation/constants'; + +// Mock dependencies +vi.mock('@fastgpt/service/core/evaluation/task', () => ({ + EvaluationTaskService: { + createEvaluation: vi.fn() + } +})); + +vi.mock('@fastgpt/service/support/permission/teamLimit', () => ({ + checkTeamAIPoints: vi.fn() +})); + +vi.mock('@fastgpt/service/core/evaluation/common', () => ({ + authEvaluationTaskCreate: vi.fn().mockResolvedValue({ + teamId: 'mock-team-id', + tmbId: 'mock-tmb-id' + }) +})); + +vi.mock('@fastgpt/service/core/evaluation/target', () => ({ + validateTargetConfig: vi.fn().mockResolvedValue({ + success: false, + message: 'Target validation failed' + }) +})); + +vi.mock('@fastgpt/service/common/system/log', () => ({ + addLog: { + info: vi.fn(), + error: vi.fn(), + warn: vi.fn() + } +})); + +describe('Create Evaluation Task API Handler', () => { + const mockEvaluation = { + _id: new Types.ObjectId(), + name: 'Test Evaluation', + description: 'Test Description', + datasetId: new Types.ObjectId(), + targetId: new Types.ObjectId(), + metricIds: [new Types.ObjectId(), new Types.ObjectId()], + usageId: new Types.ObjectId(), + status: EvaluationStatusEnum.queuing, + teamId: new Types.ObjectId(), + tmbId: new Types.ObjectId(), + createTime: new Date(), + updateTime: new Date() + }; + + beforeEach(() => { + vi.clearAllMocks(); + }); + + test('应该成功创建评估任务', async () => { + const mockReq = { + method: 'POST', + body: { + name: 'Test Evaluation', + description: 'Test Description', + datasetId: new Types.ObjectId().toString(), + target: { + type: 'workflow', + config: { + appId: new Types.ObjectId().toString() + } + }, + evaluators: [ + { + metric: { + _id: new Types.ObjectId().toString(), + name: 'Test Metric', + type: 'ai_model', + config: { llm: 'gpt-4', prompt: 'test' }, + dependencies: ['llm'], + teamId: new Types.ObjectId().toString(), + tmbId: new Types.ObjectId().toString(), + createTime: new Date(), + updateTime: new Date() + }, + runtimeConfig: { llm: 'gpt-4' } + } + ] + } + } as any; + + (checkTeamAIPoints as any).mockResolvedValue(undefined); + (validateTargetConfig as any).mockResolvedValue({ success: true, message: 'Valid' }); + (EvaluationTaskService.createEvaluation as any).mockResolvedValue(mockEvaluation); + + const result = await createHandler(mockReq); + + expect(EvaluationTaskService.createEvaluation).toHaveBeenCalledWith( + expect.objectContaining({ + name: 'Test Evaluation', + description: 'Test Description', + datasetId: mockReq.body.datasetId, + target: mockReq.body.target, + evaluators: mockReq.body.evaluators, + teamId: 'mock-team-id', + tmbId: 'mock-tmb-id' + }) + ); + expect(result).toEqual(mockEvaluation); + expect(addLog.info).toHaveBeenCalledWith( + '[Evaluation] Evaluation task created successfully', + expect.objectContaining({ + evalId: mockEvaluation._id, + name: mockEvaluation.name + }) + ); + }); + + test('应该拒绝空名称', async () => { + const mockReq = { + method: 'POST', + body: { + name: '', + datasetId: new Types.ObjectId().toString(), + target: { + type: 'workflow', + config: { + appId: new Types.ObjectId().toString() + } + }, + evaluators: [ + { + metric: { + _id: new Types.ObjectId().toString(), + name: 'Test Metric', + type: 'ai_model', + config: { llm: 'gpt-4', prompt: 'test' }, + dependencies: ['llm'], + teamId: new Types.ObjectId().toString(), + tmbId: new Types.ObjectId().toString(), + createTime: new Date(), + updateTime: new Date() + }, + runtimeConfig: { llm: 'gpt-4' } + } + ] + } + } as any; + + await expect(createHandler(mockReq)).rejects.toMatch('Evaluation name is required'); + }); + + test('应该拒绝空指标列表', async () => { + const mockReq = { + method: 'POST', + body: { + name: 'Test Evaluation', + datasetId: new Types.ObjectId().toString(), + target: { + type: 'workflow', + config: { + appId: new Types.ObjectId().toString() + } + }, + evaluators: [] + } + } as any; + + // 由于target验证在前,空的evaluators测试会被target验证拦截,这里改为测试有效target但空evaluators的情况 + // 需要mock validateTargetConfig返回成功 + (validateTargetConfig as any).mockResolvedValue({ success: true, message: 'Valid' }); + + await expect(createHandler(mockReq)).rejects.toMatch('At least one evaluator is required'); + }); + + test('应该拒绝缺少必填字段', async () => { + const mockReq = { + method: 'POST', + body: { + name: 'Test Evaluation' + // 缺少 datasetId, target, metricIds + } + } as any; + + // 重置mock为默认的失败状态 + (validateTargetConfig as any).mockResolvedValue({ + success: false, + message: 'Target validation failed' + }); + + // 由于没有target字段,target验证会先失败 + await expect(createHandler(mockReq)).rejects.toMatch('Target validation failed'); + }); +}); diff --git a/test/cases/pages/api/core/evaluation/task/delete.test.ts b/test/cases/pages/api/core/evaluation/task/delete.test.ts new file mode 100644 index 000000000000..c5e17d14ac82 --- /dev/null +++ b/test/cases/pages/api/core/evaluation/task/delete.test.ts @@ -0,0 +1,39 @@ +import { describe, test, expect, vi, beforeEach } from 'vitest'; +import { Types } from '@fastgpt/service/common/mongo'; +import { handler as deleteHandler } from '@/pages/api/core/evaluation/task/delete'; +import { EvaluationTaskService } from '@fastgpt/service/core/evaluation/task'; + +// Mock dependencies +vi.mock('@fastgpt/service/core/evaluation/task', () => ({ + EvaluationTaskService: { + deleteEvaluation: vi.fn() + } +})); + +vi.mock('@fastgpt/service/core/evaluation/common', () => ({ + authEvaluationTaskWrite: vi.fn().mockResolvedValue({ + teamId: 'mock-team-id', + tmbId: 'mock-tmb-id' + }) +})); + +describe('Delete Evaluation Task API Handler', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + test('应该成功删除评估任务', async () => { + const evalId = new Types.ObjectId().toString(); + const mockReq = { + method: 'DELETE', + query: { evalId: evalId } + } as any; + + (EvaluationTaskService.deleteEvaluation as any).mockResolvedValue(undefined); + + const result = await deleteHandler(mockReq); + + expect(EvaluationTaskService.deleteEvaluation).toHaveBeenCalledWith(evalId, 'mock-team-id'); + expect(result).toEqual({ message: 'Evaluation deleted successfully' }); + }); +}); diff --git a/test/cases/pages/api/core/evaluation/task/detail.test.ts b/test/cases/pages/api/core/evaluation/task/detail.test.ts new file mode 100644 index 000000000000..ab640b9682c7 --- /dev/null +++ b/test/cases/pages/api/core/evaluation/task/detail.test.ts @@ -0,0 +1,57 @@ +import { describe, test, expect, vi, beforeEach } from 'vitest'; +import { Types } from '@fastgpt/service/common/mongo'; +import { handler as detailHandler } from '@/pages/api/core/evaluation/task/detail'; +import { EvaluationTaskService } from '@fastgpt/service/core/evaluation/task'; + +// Mock dependencies +vi.mock('@fastgpt/service/core/evaluation/task', () => ({ + EvaluationTaskService: { + getEvaluation: vi.fn() + } +})); + +vi.mock('@fastgpt/service/core/evaluation/common', () => ({ + authEvaluationTaskRead: vi.fn().mockResolvedValue({ + teamId: 'mock-team-id', + tmbId: 'mock-tmb-id', + evaluation: { + _id: 'mock-eval-id', + name: 'Mock Evaluation', + status: 'completed' + } + }) +})); + +describe('Get Evaluation Task Detail API Handler', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + test('应该成功获取评估任务详情', async () => { + const evalId = new Types.ObjectId().toString(); + const mockReq = { + method: 'GET', + query: { evalId: evalId } + } as any; + + const result = await detailHandler(mockReq); + + // The detail handler no longer calls getEvaluation, it gets data from authEvaluationTaskRead + expect(result).toEqual( + expect.objectContaining({ + _id: 'mock-eval-id', + name: 'Mock Evaluation', + status: 'completed' + }) + ); + }); + + test('应该拒绝缺少ID的请求', async () => { + const mockReq = { + method: 'GET', + query: {} + } as any; + + await expect(detailHandler(mockReq)).rejects.toMatch('Evaluation ID is required'); + }); +}); diff --git a/test/cases/pages/api/core/evaluation/task/list.test.ts b/test/cases/pages/api/core/evaluation/task/list.test.ts new file mode 100644 index 000000000000..ee2178381dec --- /dev/null +++ b/test/cases/pages/api/core/evaluation/task/list.test.ts @@ -0,0 +1,137 @@ +import { describe, test, expect, vi, beforeEach } from 'vitest'; +import { Types } from '@fastgpt/service/common/mongo'; +import { handler as listHandler } from '@/pages/api/core/evaluation/task/list'; +import { EvaluationTaskService } from '@fastgpt/service/core/evaluation/task'; +import { EvaluationStatusEnum } from '@fastgpt/global/core/evaluation/constants'; + +// Mock dependencies +vi.mock('@fastgpt/service/core/evaluation/task', () => ({ + EvaluationTaskService: { + listEvaluations: vi.fn() + } +})); + +vi.mock('@fastgpt/service/core/evaluation/common', () => ({ + getEvaluationPermissionAggregation: vi.fn().mockResolvedValue({ + teamId: 'mock-team-id', + tmbId: 'mock-tmb-id', + isOwner: true, + roleList: [], + myGroupMap: new Map(), + myOrgSet: new Set() + }) +})); + +// Mock additional modules for permission handling +vi.mock('@fastgpt/global/support/permission/evaluation/controller', () => ({ + EvaluationPermission: vi.fn().mockImplementation(() => ({ + hasReadPer: true + })) +})); + +vi.mock('@fastgpt/service/support/user/utils', () => ({ + addSourceMember: vi.fn().mockImplementation(({ list }) => Promise.resolve(list)) +})); + +describe('List Evaluation Tasks API Handler', () => { + const mockEvaluation = { + _id: new Types.ObjectId(), + name: 'Test Evaluation', + description: 'Test Description', + datasetId: new Types.ObjectId(), + targetId: new Types.ObjectId(), + metricIds: [new Types.ObjectId(), new Types.ObjectId()], + usageId: new Types.ObjectId(), + status: EvaluationStatusEnum.queuing, + teamId: new Types.ObjectId(), + tmbId: new Types.ObjectId(), + createTime: new Date(), + updateTime: new Date() + }; + + beforeEach(() => { + vi.clearAllMocks(); + }); + + test('应该成功获取评估任务列表', async () => { + const mockReq = { + body: { + pageNum: 1, + pageSize: 10 + } + } as any; + + const mockResult = { + list: [mockEvaluation], + total: 1 + }; + + (EvaluationTaskService.listEvaluations as any).mockResolvedValue(mockResult); + + const result = await listHandler(mockReq); + + expect(EvaluationTaskService.listEvaluations).toHaveBeenCalledWith( + 'mock-team-id', + 1, + 10, + undefined, + [], + 'mock-tmb-id', + true + ); + expect(result).toEqual({ + list: mockResult.list.map((item) => ({ + ...item, + permission: { hasReadPer: true }, + private: true + })), + total: mockResult.total + }); + }); + + test('应该处理搜索参数', async () => { + const mockReq = { + body: { + pageNum: 2, + pageSize: 20, + searchKey: 'test search' + } + } as any; + + const mockResult = { list: [], total: 0 }; + (EvaluationTaskService.listEvaluations as any).mockResolvedValue(mockResult); + + await listHandler(mockReq); + + expect(EvaluationTaskService.listEvaluations).toHaveBeenCalledWith( + 'mock-team-id', + 2, + 20, + 'test search', + [], + 'mock-tmb-id', + true + ); + }); + + test('应该使用默认分页参数', async () => { + const mockReq = { + body: {} + } as any; + + const mockResult = { list: [], total: 0 }; + (EvaluationTaskService.listEvaluations as any).mockResolvedValue(mockResult); + + await listHandler(mockReq); + + expect(EvaluationTaskService.listEvaluations).toHaveBeenCalledWith( + 'mock-team-id', + 1, + 20, + undefined, + [], + 'mock-tmb-id', + true + ); + }); +}); diff --git a/test/cases/pages/api/core/evaluation/task/start.test.ts b/test/cases/pages/api/core/evaluation/task/start.test.ts new file mode 100644 index 000000000000..34d11071496e --- /dev/null +++ b/test/cases/pages/api/core/evaluation/task/start.test.ts @@ -0,0 +1,48 @@ +import { describe, test, expect, vi, beforeEach } from 'vitest'; +import { Types } from '@fastgpt/service/common/mongo'; +import { handler as startHandler } from '@/pages/api/core/evaluation/task/start'; +import { EvaluationTaskService } from '@fastgpt/service/core/evaluation/task'; + +// Mock dependencies +vi.mock('@fastgpt/service/core/evaluation/task', () => ({ + EvaluationTaskService: { + startEvaluation: vi.fn() + } +})); + +vi.mock('@fastgpt/service/core/evaluation/common', () => ({ + authEvaluationTaskExecution: vi.fn().mockResolvedValue({ + teamId: 'mock-team-id', + tmbId: 'mock-tmb-id' + }) +})); + +describe('Start Evaluation Task API Handler', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + test('应该成功启动评估任务', async () => { + const evalId = new Types.ObjectId().toString(); + const mockReq = { + method: 'POST', + body: { evalId } + } as any; + + (EvaluationTaskService.startEvaluation as any).mockResolvedValue(undefined); + + const result = await startHandler(mockReq); + + expect(EvaluationTaskService.startEvaluation).toHaveBeenCalledWith(evalId, 'mock-team-id'); + expect(result).toEqual({ message: 'Evaluation started successfully' }); + }); + + test('应该拒绝缺少评估ID的请求', async () => { + const mockReq = { + method: 'POST', + body: {} + } as any; + + await expect(startHandler(mockReq)).rejects.toMatch('Evaluation ID is required'); + }); +}); diff --git a/test/cases/pages/api/core/evaluation/task/stats.test.ts b/test/cases/pages/api/core/evaluation/task/stats.test.ts new file mode 100644 index 000000000000..ec14d1261412 --- /dev/null +++ b/test/cases/pages/api/core/evaluation/task/stats.test.ts @@ -0,0 +1,57 @@ +import { describe, test, expect, vi, beforeEach } from 'vitest'; +import { Types } from '@fastgpt/service/common/mongo'; +import { handler as statsHandler } from '@/pages/api/core/evaluation/task/stats'; +import { EvaluationTaskService } from '@fastgpt/service/core/evaluation/task'; + +// Mock dependencies +vi.mock('@fastgpt/service/core/evaluation/task', () => ({ + EvaluationTaskService: { + getEvaluationStats: vi.fn() + } +})); + +vi.mock('@fastgpt/service/core/evaluation/common', () => ({ + authEvaluationTaskRead: vi.fn().mockResolvedValue({ + teamId: 'mock-team-id', + tmbId: 'mock-tmb-id' + }) +})); + +describe('Get Evaluation Task Stats API Handler', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + test('应该成功获取评估任务统计信息', async () => { + const evalId = new Types.ObjectId().toString(); + const mockReq = { + method: 'GET', + query: { evalId } + } as any; + + const mockStats = { + total: 100, + completed: 80, + evaluating: 10, + queuing: 5, + error: 5, + avgScore: 85.5 + }; + + (EvaluationTaskService.getEvaluationStats as any).mockResolvedValue(mockStats); + + const result = await statsHandler(mockReq); + + expect(EvaluationTaskService.getEvaluationStats).toHaveBeenCalledWith(evalId, 'mock-team-id'); + expect(result).toEqual(mockStats); + }); + + test('应该拒绝缺少评估ID的请求', async () => { + const mockReq = { + method: 'GET', + query: {} + } as any; + + await expect(statsHandler(mockReq)).rejects.toMatch('Evaluation ID is required'); + }); +}); diff --git a/test/cases/pages/api/core/evaluation/task/stop.test.ts b/test/cases/pages/api/core/evaluation/task/stop.test.ts new file mode 100644 index 000000000000..5bbc7962d5c5 --- /dev/null +++ b/test/cases/pages/api/core/evaluation/task/stop.test.ts @@ -0,0 +1,39 @@ +import { describe, test, expect, vi, beforeEach } from 'vitest'; +import { Types } from '@fastgpt/service/common/mongo'; +import { handler as stopHandler } from '@/pages/api/core/evaluation/task/stop'; +import { EvaluationTaskService } from '@fastgpt/service/core/evaluation/task'; + +// Mock dependencies +vi.mock('@fastgpt/service/core/evaluation/task', () => ({ + EvaluationTaskService: { + stopEvaluation: vi.fn() + } +})); + +vi.mock('@fastgpt/service/core/evaluation/common', () => ({ + authEvaluationTaskExecution: vi.fn().mockResolvedValue({ + teamId: 'mock-team-id', + tmbId: 'mock-tmb-id' + }) +})); + +describe('Stop Evaluation Task API Handler', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + test('应该成功停止评估任务', async () => { + const evalId = new Types.ObjectId().toString(); + const mockReq = { + method: 'POST', + body: { evalId } + } as any; + + (EvaluationTaskService.stopEvaluation as any).mockResolvedValue(undefined); + + const result = await stopHandler(mockReq); + + expect(EvaluationTaskService.stopEvaluation).toHaveBeenCalledWith(evalId, 'mock-team-id'); + expect(result).toEqual({ message: 'Evaluation stopped successfully' }); + }); +}); diff --git a/test/cases/pages/api/core/evaluation/task/update.test.ts b/test/cases/pages/api/core/evaluation/task/update.test.ts new file mode 100644 index 000000000000..aadaa3b49e28 --- /dev/null +++ b/test/cases/pages/api/core/evaluation/task/update.test.ts @@ -0,0 +1,50 @@ +import { describe, test, expect, vi, beforeEach } from 'vitest'; +import { Types } from '@fastgpt/service/common/mongo'; +import { handler as updateHandler } from '@/pages/api/core/evaluation/task/update'; +import { EvaluationTaskService } from '@fastgpt/service/core/evaluation/task'; + +// Mock dependencies +vi.mock('@fastgpt/service/core/evaluation/task', () => ({ + EvaluationTaskService: { + updateEvaluation: vi.fn() + } +})); + +vi.mock('@fastgpt/service/core/evaluation/common', () => ({ + authEvaluationTaskWrite: vi.fn().mockResolvedValue({ + teamId: 'mock-team-id', + tmbId: 'mock-tmb-id' + }) +})); + +describe('Update Evaluation Task API Handler', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + test('应该成功更新评估任务', async () => { + const evalId = new Types.ObjectId().toString(); + const mockReq = { + method: 'PUT', + body: { + evalId: evalId, + name: 'Updated Evaluation', + description: 'Updated Description' + } + } as any; + + (EvaluationTaskService.updateEvaluation as any).mockResolvedValue(undefined); + + const result = await updateHandler(mockReq); + + expect(EvaluationTaskService.updateEvaluation).toHaveBeenCalledWith( + evalId, + expect.objectContaining({ + name: 'Updated Evaluation', + description: 'Updated Description' + }), + 'mock-team-id' + ); + expect(result).toEqual({ message: 'Evaluation updated successfully' }); + }); +});