diff --git a/packages/global/common/error/code/database.ts b/packages/global/common/error/code/database.ts index f313496b2b6b..b6cc41b38492 100644 --- a/packages/global/common/error/code/database.ts +++ b/packages/global/common/error/code/database.ts @@ -1,3 +1,4 @@ +import { i18nT } from '../../../../web/i18n/utils'; import { type ErrType } from '../errorCode'; /* database: 509000 */ @@ -10,42 +11,32 @@ export enum DatabaseErrEnum { clientDestroyError = 'databaseClientDestroyError', clientAlreadyExists = 'databaseClientAlreadyExists', clientNotFound = 'databaseClientNotFound', - + // 连接相关错误 authError = 'databaseAuthError', - nameError = 'databaseNameError', - addressError = 'databaseAddressError', + databaseNameError = 'databaseNameError', + databasePortError = 'databasePortError', + hostError = 'databaseHostError', checkError = 'databaseCheckError', + econnRefused = 'connectionRefused', connectionFailed = 'databaseConnectionFailed', connectionTimeout = 'databaseConnectionTimeout', - + connectionLost = 'databaseConnectionLost', + // 数据库类型和支持错误 notSupportType = 'databaseNotSupportType', notImplemented = 'databaseNotImplemented', - + // API 请求和验证错误 requestValidationError = 'databaseRequestValidationError', invalidTableName = 'databaseInvalidTableName', fetchInfoError = 'databaseFetchInfoError', - invalidConfig = 'databaseInvalidConfig', - - // 查询和操作错误 - queryExecutionError = 'databaseQueryExecutionError', - tableNotFound = 'databaseTableNotFound', - columnNotFound = 'databaseColumnNotFound', - syntaxError = 'databaseSyntaxError', - - // Schema相关错误 - schemaIntrospectionError = 'databaseSchemaIntrospectionError', - metadataError = 'databaseMetadataError' + dbConfigNotFound = 'databaseConfigNotFound', + opUnknownDatabaseError = 'opUnknownDatabaseError', + dativeServiceError = 'dativeServiceError' } const databaseErr = [ - - { - statusText: DatabaseErrEnum.datasetParamsError, - message: 'core.database.error.not_support_dataset_type' - }, // 客户端管理错误 { statusText: DatabaseErrEnum.clientCreateError, @@ -57,97 +48,81 @@ const databaseErr = [ }, { statusText: DatabaseErrEnum.clientDestroyError, - message: 'core.database.error.client_destroy_failed' - }, - { - statusText: DatabaseErrEnum.clientAlreadyExists, - message: 'core.database.error.client_already_exists' + message: i18nT('database_client:client_destory_error') }, { statusText: DatabaseErrEnum.clientNotFound, - message: 'core.database.error.client_not_found' + message: i18nT('database_client:client_not_found') }, - + // 连接错误 { statusText: DatabaseErrEnum.authError, - message: 'core.database.error.auth_failed' + message: i18nT('database_client:authentication_failed') + }, + { + statusText: DatabaseErrEnum.databaseNameError, + message: i18nT('database_client:database_not_exist') }, { - statusText: DatabaseErrEnum.nameError, - message: 'core.database.error.database_not_found' + statusText: DatabaseErrEnum.databasePortError, + message: i18nT('database_client:database_port_error') }, { - statusText: DatabaseErrEnum.addressError, - message: 'core.database.error.connection_address_failed' + statusText: DatabaseErrEnum.hostError, + message: i18nT('database_client:host_error') + }, + { + statusText: DatabaseErrEnum.econnRefused, + message: i18nT('database_client:connection_refused') }, { statusText: DatabaseErrEnum.checkError, - message: 'core.database.error.connection_check_failed' + message: i18nT('database_client:connection_check_error') + }, + { + statusText: DatabaseErrEnum.connectionLost, + message: i18nT('database_client:connection_lost') }, { statusText: DatabaseErrEnum.connectionFailed, - message: 'core.database.error.connection_failed' + message: i18nT('database_client:connection_failed') }, { statusText: DatabaseErrEnum.connectionTimeout, - message: 'core.database.error.connection_timeout' + message: i18nT('database_client:connection_timeout') }, - + // 类型支持错误 { statusText: DatabaseErrEnum.notSupportType, - message: 'core.database.error.database_type_not_supported' + message: i18nT('database_client:not_support_databaseType') }, { statusText: DatabaseErrEnum.notImplemented, - message: 'core.database.error.feature_not_implemented' + message: i18nT('database_client:not_implemented_databaseType') }, - + // 请求验证错误 - { - statusText: DatabaseErrEnum.requestValidationError, - message: 'core.database.error.request_validation_failed' - }, { statusText: DatabaseErrEnum.invalidTableName, - message: 'core.database.error.invalid_table_name' + message: i18nT('database_client:invalid_table_name') }, { statusText: DatabaseErrEnum.fetchInfoError, - message: 'core.database.error.fetch_info_failed' - }, - { - statusText: DatabaseErrEnum.invalidConfig, - message: 'core.database.error.invalid_config' - }, - - // 查询操作错误 - { - statusText: DatabaseErrEnum.queryExecutionError, - message: 'core.database.error.query_execution_failed' - }, - { - statusText: DatabaseErrEnum.tableNotFound, - message: 'core.database.error.table_not_found' - }, - { - statusText: DatabaseErrEnum.columnNotFound, - message: 'core.database.error.column_not_found' + message: i18nT('database_client:fetch_info_error') }, { - statusText: DatabaseErrEnum.syntaxError, - message: 'core.database.error.sql_syntax_error' + statusText: DatabaseErrEnum.dbConfigNotFound, + message: i18nT('database_client:database_config_not_found') }, - - // Schema错误 { - statusText: DatabaseErrEnum.schemaIntrospectionError, - message: 'core.database.error.schema_introspection_failed' + statusText: DatabaseErrEnum.opUnknownDatabaseError, + message: i18nT('database_client:op_unknown_database_error') }, { - statusText: DatabaseErrEnum.metadataError, - message: 'core.database.error.metadata_error' + statusText: DatabaseErrEnum.dativeServiceError, + message: i18nT('database_client:dative_service_error') } ]; diff --git a/packages/global/common/error/code/evaluation.ts b/packages/global/common/error/code/evaluation.ts index b12374d52897..0795ee9d7c99 100644 --- a/packages/global/common/error/code/evaluation.ts +++ b/packages/global/common/error/code/evaluation.ts @@ -8,7 +8,6 @@ export enum EvaluationErrEnum { evalNameTooLong = 'evaluationNameTooLong', evalDescriptionTooLong = 'evaluationDescriptionTooLong', evalDescriptionInvalidType = 'evaluationDescriptionInvalidType', - evalDatasetIdRequired = 'evaluationDatasetIdRequired', evalTargetRequired = 'evaluationTargetRequired', evalTargetInvalidConfig = 'evaluationTargetInvalidConfig', evalTargetAppIdMissing = 'evaluationTargetAppIdMissing', @@ -31,10 +30,7 @@ export enum EvaluationErrEnum { evalInvalidStatus = 'evaluationInvalidStatus', evalInvalidStateTransition = 'evaluationInvalidStateTransition', evalOnlyRunningCanStop = 'evaluationOnlyRunningCanStop', - evalOnlyFailedCanRetry = 'evaluationOnlyFailedCanRetry', evalItemNoErrorToRetry = 'evaluationItemNoErrorToRetry', - evalTargetOutputRequired = 'evaluationTargetOutputRequired', - evalEvaluatorOutputRequired = 'evaluationEvaluatorOutputRequired', evalDatasetLoadFailed = 'evaluationDatasetLoadFailed', evalTargetConfigInvalid = 'evaluationTargetConfigInvalid', evalEvaluatorsConfigInvalid = 'evaluationEvaluatorsConfigInvalid', @@ -43,7 +39,11 @@ export enum EvaluationErrEnum { evalDuplicateDatasetName = 'evaluationDuplicateDatasetName', evalNoDataInCollections = 'evaluationNoDataInCollections', evalUpdateFailed = 'evaluationUpdateFailed', - evalLockAcquisitionFailed = 'evaluationLockAcquisitionFailed', + // Task execution errors + evalTaskSystemError = 'evaluationTaskSystemError', + evalManuallyStopped = 'evaluationManuallyStopped', + evalEvaluatorExecutionErrors = 'evaluationEvaluatorExecutionErrors', + evalTargetExecutionError = 'evaluationTargetExecutionError', // Metric related errors evalMetricNotFound = 'evaluationMetricNotFound', @@ -55,6 +55,7 @@ export enum EvaluationErrEnum { evalMetricPromptTooLong = 'evaluationMetricPromptTooLong', evalMetricTypeRequired = 'evaluationMetricTypeRequired', evalMetricTypeInvalid = 'evaluationMetricTypeInvalid', + evalMetricNameInvalid = 'evaluationMetricNameInvalid', evalMetricBuiltinCannotModify = 'evaluationMetricBuiltinCannotModify', evalMetricBuiltinCannotDelete = 'evaluationMetricBuiltinCannotDelete', evalMetricIdRequired = 'evaluationMetricIdRequired', @@ -118,11 +119,13 @@ export enum EvaluationErrEnum { datasetTaskDeleteFailed = 'evaluationDatasetTaskDeleteFailed', fetchFailedTasksError = 'evaluationFetchFailedTasksError', + // Evaluation task job related errors + evalItemJobNotFound = 'evaluationItemJobNotFound', + // File/Import related errors fileIdRequired = 'evaluationFileIdRequired', fileMustBeCSV = 'evaluationFileMustBeCSV', csvInvalidStructure = 'evaluationCSVInvalidStructure', - csvTooManyRows = 'evaluationCSVTooManyRows', csvParsingError = 'evaluationCSVParsingError', csvNoDataRows = 'evaluationCSVNoDataRows', @@ -172,10 +175,6 @@ const evaluationErrList = [ statusText: EvaluationErrEnum.evalDescriptionTooLong, message: i18nT('evaluation:description_too_long') }, - { - statusText: EvaluationErrEnum.evalDatasetIdRequired, - message: i18nT('evaluation:dataset_id_required') - }, { statusText: EvaluationErrEnum.evalTargetRequired, message: i18nT('evaluation:target_required') @@ -252,21 +251,13 @@ const evaluationErrList = [ statusText: EvaluationErrEnum.evalOnlyRunningCanStop, message: i18nT('evaluation:only_running_can_stop') }, - { - statusText: EvaluationErrEnum.evalOnlyFailedCanRetry, - message: i18nT('evaluation:only_failed_can_retry') - }, { statusText: EvaluationErrEnum.evalItemNoErrorToRetry, message: i18nT('evaluation:item_no_error_to_retry') }, { - statusText: EvaluationErrEnum.evalTargetOutputRequired, - message: i18nT('evaluation:target_output_required') - }, - { - statusText: EvaluationErrEnum.evalEvaluatorOutputRequired, - message: i18nT('evaluation:evaluator_output_required') + statusText: EvaluationErrEnum.evalTargetExecutionError, + message: i18nT('evaluation:target_execution_error') }, { statusText: EvaluationErrEnum.evalDatasetLoadFailed, @@ -300,10 +291,6 @@ const evaluationErrList = [ statusText: EvaluationErrEnum.evalUpdateFailed, message: i18nT('evaluation:update_failed') }, - { - statusText: EvaluationErrEnum.evalLockAcquisitionFailed, - message: i18nT('evaluation:lock_acquisition_failed') - }, // Metric related errors { statusText: EvaluationErrEnum.evalMetricNotFound, @@ -341,6 +328,10 @@ const evaluationErrList = [ statusText: EvaluationErrEnum.evalMetricTypeInvalid, message: i18nT('evaluation:metric_type_invalid') }, + { + statusText: EvaluationErrEnum.evalMetricNameInvalid, + message: i18nT('evaluation:metric_name_invalid') + }, { statusText: EvaluationErrEnum.evalMetricBuiltinCannotModify, message: i18nT('evaluation:metric_builtin_cannot_modify') @@ -540,6 +531,12 @@ const evaluationErrList = [ message: i18nT('evaluation:fetch_failed_tasks_error') }, + // Evaluation task job related errors + { + statusText: EvaluationErrEnum.evalItemJobNotFound, + message: i18nT('evaluation:item_job_not_found') + }, + // File/Import related errors { statusText: EvaluationErrEnum.fileIdRequired, @@ -553,10 +550,6 @@ const evaluationErrList = [ statusText: EvaluationErrEnum.csvInvalidStructure, message: i18nT('evaluation:csv_invalid_structure') }, - { - statusText: EvaluationErrEnum.csvTooManyRows, - message: i18nT('evaluation:csv_too_many_rows') - }, { statusText: EvaluationErrEnum.csvParsingError, message: i18nT('evaluation:csv_parsing_error') @@ -634,6 +627,20 @@ const evaluationErrList = [ { statusText: EvaluationErrEnum.evalDescriptionInvalidType, message: i18nT('evaluation:description_invalid_type') + }, + + // Task execution errors + { + statusText: EvaluationErrEnum.evalTaskSystemError, + message: i18nT('evaluation:task_system_error') + }, + { + statusText: EvaluationErrEnum.evalManuallyStopped, + message: i18nT('evaluation:manually_stopped') + }, + { + statusText: EvaluationErrEnum.evalEvaluatorExecutionErrors, + message: i18nT('evaluation:evaluator_execution_errors') } ]; diff --git a/packages/global/common/error/utils.ts b/packages/global/common/error/utils.ts index c13c8e2666cd..f21cc46d87d9 100644 --- a/packages/global/common/error/utils.ts +++ b/packages/global/common/error/utils.ts @@ -28,3 +28,15 @@ export class UserError extends Error { this.name = 'UserError'; } } + +export class FileUploadError extends UserError { + code: string; + details?: Record; + + constructor(code: string, message: string, details?: Record) { + super(message); + this.name = 'FileUploadError'; + this.code = code; + this.details = details; + } +} diff --git a/packages/global/common/file/constants.ts b/packages/global/common/file/constants.ts index abd996f1d7ed..3d7832675d54 100644 --- a/packages/global/common/file/constants.ts +++ b/packages/global/common/file/constants.ts @@ -27,3 +27,18 @@ export const ReadFileBaseUrl = `${EndpointUrl}/api/common/file/read`; export const documentFileType = '.txt, .docx, .csv, .xlsx, .pdf, .md, .html, .pptx'; export const imageFileType = '.jpg, .jpeg, .png, .gif, .bmp, .webp, .svg, .tiff, .tif, .ico, .heic, .heif, .avif, .raw, .cr2, .nef, .arw, .dng, .psd, .ai, .eps, .emf, .wmf, .jfif, .exif, .pgm, .ppm, .pbm, .jp2, .j2k, .jpf, .jpx, .jpm, .mj2, .xbm, .pcx'; + +/* File Upload Limits */ +export const DEFAULT_FILE_UPLOAD_LIMITS = { + // Default maximum number of files that can be uploaded at once + MAX_FILE_COUNT: 20, + // Default maximum size per file in MB + MAX_FILE_SIZE_MB: 500 +} as const; + +/* File Upload Error Codes */ +export enum FileUploadErrorEnum { + FILE_TOO_LARGE = 'FILE_TOO_LARGE', + TOO_MANY_FILES = 'TOO_MANY_FILES', + INVALID_FILE_TYPE = 'INVALID_FILE_TYPE' +} diff --git a/packages/global/common/system/types/index.d.ts b/packages/global/common/system/types/index.d.ts index 4b78f5f0c455..0a6188c11844 100644 --- a/packages/global/common/system/types/index.d.ts +++ b/packages/global/common/system/types/index.d.ts @@ -129,6 +129,7 @@ export type FastGPTFeConfigsType = { export type SystemEnvType = { openapiPrefix?: string; + parseMaxProcess?: number; vectorMaxProcess: number; qaMaxProcess: number; vlmMaxProcess: number; @@ -141,15 +142,31 @@ export type SystemEnvType = { chatApiKey?: string; customPdfParse?: customPdfParseType; + + // Evaluation configurations + evalConfig?: EvaluationConfigType; }; export type customPdfParseType = { url?: string; key?: string; + timeout?: number; doc2xKey?: string; price?: number; }; +export type EvaluationConfigType = { + taskConcurrency?: number; + caseConcurrency?: number; + caseMaxRetry?: number; + caseResultThreshold?: number; + summaryConcurrency?: number; + dataQualityConcurrency?: number; + datasetDataSynthesizeConcurrency?: number; + datasetSmartGenerateConcurrency?: number; + maxStalledCount?: number; +}; + export type LicenseDataType = { startTime: string; expiredTime: string; diff --git a/packages/global/core/ai/prompt/eval.ts b/packages/global/core/ai/prompt/eval.ts index 11b0d4eb0e5c..9f6f5fa4a371 100644 --- a/packages/global/core/ai/prompt/eval.ts +++ b/packages/global/core/ai/prompt/eval.ts @@ -1,16 +1,43 @@ -const evalSummaryTemplate = `角色:你是一个数据分析专家,擅长分析评测数据并进行总结 -任务: 对给出的评估结果列表进行精简的总结,假设具备足够的领域知识 -要求: 生成的总结(字符串),要求如下: -1.输出尽量简洁,和给出的评估结果保持一致并提供了足够的维度来概括总结,根据总结用户能够了解评估数据的概况,不要超过200字 -2.参考以下格式化输出 -{example} -输入: -{evaluation_result_for_single_metric} -输出: -`; -const goodExample = `如:整体表现良好,大部分回答都能准 -回应用户需求,表现出较高的准确性和相关性。`; - -const badExample = `如:存在明显问题,部分回答偏离主题或存在事实错误,如xx说法回答存在`; - -export { evalSummaryTemplate, goodExample, badExample }; +const problemAnalysisTemplate = `你是一名问题诊断专家,专注于分析AI系统的缺陷和错误模式,假设具备足够的领域知识。 +##任务 +基于评估原因,对AI表现中暴露的核心问题进行诊断,识别关键错误类型和频发模式。 + +##输出要求 +1. 控制在150字以内,突出最重要的缺陷发现,直接返回正文,不能携带标题 +2. 明确指出错误类型、表现模式和影响 +3. 结合评估原因进行具体诊断,避免笼统描述 +4. 输出应具备洞察力和价值,而非简单复述,但无需给优化建议 +5. 返回总结用户能够了解评估数据的概况 + +##参考示例 +{example} + +##评估数据 +{evaluation_result_for_single_metric} + +开始返回问题分析总结:`; + +const strengthAnalysisTemplate = `你是一名优势分析专家,专注于识别AI系统的优秀表现和成功模式,假设具备足够的领域知识。 +##任务 +基于评估原因,总结AI在本次表现中的优势,提炼可复制的成功因子。 + +##输出要求 +1. 控制在150字以内,突出最关键的优势,直接返回正文,不用携带标题 +2. 明确描述优势的具体表现和成功模式 +3. 强调可推广的优点和最佳实践 +4. 基于评估原因提炼洞察,避免空洞或泛泛而谈 +5. 返回总结用户能够了解评估数据的概况 + +##参考示例 +{example} + +##评估数据 +{evaluation_result_for_single_metric} + +开始返回优势分析总结:`; + +const goodExample = `回答准确性高,逻辑条理清晰,能正确理解用户意图并给出相关解答。在复杂问题处理上展现出良好的上下文理解与知识整合能力,优势主要体现在逻辑推理严谨、信息提取完整、回答结构化程度高。`; + +const badExample = `存在理解偏差、信息遗漏和逻辑错误。如xx表现体现复杂指令理解不足,常忽略关键要求;知识整合不连贯,导致答案不全面;多步骤推理中出现逻辑跳跃。这些问题暴露出指令解析和推理能力的缺陷。`; + +export { problemAnalysisTemplate, strengthAnalysisTemplate, goodExample, badExample }; diff --git a/packages/global/core/app/type.d.ts b/packages/global/core/app/type.d.ts index a3514d88302f..c8405cfc3f68 100644 --- a/packages/global/core/app/type.d.ts +++ b/packages/global/core/app/type.d.ts @@ -86,6 +86,9 @@ export type AppDatasetSearchParamsType = { datasetSearchUsingExtensionQuery?: boolean; datasetSearchExtensionModel?: string; datasetSearchExtensionBg?: string; + + // database + generateSqlModel?: string; }; export type AppSimpleEditFormType = { // templateId: string; diff --git a/packages/global/core/app/utils.ts b/packages/global/core/app/utils.ts index e967f758454f..81e0cb993ba2 100644 --- a/packages/global/core/app/utils.ts +++ b/packages/global/core/app/utils.ts @@ -138,6 +138,10 @@ export const appWorkflow2Form = ({ node.inputs, NodeInputKeyEnum.datasetSearchExtensionBg ); + defaultAppForm.dataset.generateSqlModel = findInputValueByKey( + node.inputs, + NodeInputKeyEnum.generateSqlModel + ); } else if ( node.flowNodeType === FlowNodeTypeEnum.pluginModule || node.flowNodeType === FlowNodeTypeEnum.appModule || diff --git a/packages/global/core/dataset/api.d.ts b/packages/global/core/dataset/api.d.ts index dd2ebb5d4a19..18ed0f51724c 100644 --- a/packages/global/core/dataset/api.d.ts +++ b/packages/global/core/dataset/api.d.ts @@ -70,51 +70,9 @@ export type CreateDatasetCollectionParams = DatasetCollectionStoreDataType & { createTime?: Date; updateTime?: Date; tableSchema?: TableSchemaType; + forbid?: boolean; }; -export type UpdateConfigurationParams = { - databaseconfig?: { - type: { - client:{ - type: String, - required: true - }, - version:{ - type: String, - default: "5.7.44" - }, - host: { - type: String, - required: true - }, - port: { - type: Number, - default: 3306 - }, - database: { - type: String, - required: true - }, - user: { - type: String, - required: true - }, - password: { - type: String, - required: true - }, - encrypt:{ - type: Boolean, - default: true - }, - poolSize: { - type: Number, - default: 20 - } - } - }, - -} export type ApiCreateDatasetCollectionParams = DatasetCollectionStoreDataType & { datasetId: string; tags?: string[]; diff --git a/packages/global/core/dataset/database/api.d.ts b/packages/global/core/dataset/database/api.d.ts new file mode 100644 index 000000000000..622e74bd2d19 --- /dev/null +++ b/packages/global/core/dataset/database/api.d.ts @@ -0,0 +1,156 @@ +import type { ColumnSchemaType, TableSchemaType, DatabaseConfig } from '../type'; +import { ConstraintSchemaType, ForeignKeySchemaType } from '../type'; +/*-------API Request & Response Types-------*/ + +export type CheckConnectionBody = { + datasetId: string; + databaseConfig: DatabaseConfig; +}; + +/*-------Create Database Collections Type-------*/ +export type DatabaseCollectionsTable = Omit & { forbid: boolean }; + +export type DatabaseCollectionsBody = { + tables: DatabaseCollectionsTable[]; +}; + +export type CreateDatabaseCollectionsBody = DatabaseCollectionsBody & { datasetId: string }; + +export type CreateDatabaseCollectionsResponse = { + collectionIds: string[]; +}; + +/*-------Detect Changes Type-------*/ +export enum StatusEnum { + add = 'add', + delete = 'delete', + available = 'available' +} + +export type DBTableColumn = ColumnSchemaType & { status: StatusEnum }; + +export type DBTableChange = Omit & { + forbid: boolean; + status: StatusEnum; + columns: Record; +}; + +export type DetectChangesQuery = { + datasetId: string; +}; + +export type DetectChangesResponse = { + tables: DBTableChange[]; + hasChanges: boolean; + summary: { + addedTables: number; + deletedTables: number; + modifiedTables: number; + addedColumns: number; + deletedColumns: number; + }; +}; +/*-------Apply Changes Type-------*/ +export type ApplyChangesBody = { + datasetId: string; + tables: Array; +}; + +export type ApplyChangesResponse = { + success: boolean; + processedItems: { + deletedTables: number; + updatedTables: number; + addedTables: number; + affectedDataRecords: number; + }; + errors: Array<{ + type: 'table' | 'column' | 'data'; + target: string; + error: string; + }>; + taskId?: string; +}; + +/*-------Database Search Test Type-------*/ +export type DatabaseSearchTestBody = { + datasetId: string; + query: string; + model?: string; +}; + +/*-------Dativate Retrieval Type-------*/ + +export type DativeCostraintKey = { + name: string; + column: string; +}; +export type DativeForeignKey = DativeCostraintKey & { + referenced_schema: string; + referenced_table: string; + referenced_column: string; +}; +export type DativeTableColumns = { + name: string; + type: string; + comment: string; + auto_increment: boolean; + nullable: boolean; + default: any; + examples: Array; + enabled: boolean; + value_index: boolean; +}; +export type DativeTable = { + name: string; + ns_name?: string; + comment: string; + columns: Record; + primary_keys: Array; + foreign_keys: Array; + enable: boolean; + score: number; +}; + +export type DativeSchema = { + name: string; // databaseName + comments?: string; + tables: Array; +}; + +// SQL Generation types +export type SqlGenerationRequest = { + source_config: { + type: string; + host: string; + port: number; + username: string; + password: string; + db_name: string; + }; + generate_sql_llm: { + model: string; + api_key?: string; + base_url?: string; + }; + evaluate_sql_llm: { + model: string; + api_key?: string; + base_url?: string; + }; + query: string; + result_num_limit: number; + retrieved_metadata?: DativeSchema; + evidence?: string; +}; + +export type SqlGenerationResponse = { + answer: string; + sql: string; + sql_res: { + data: any[]; + columns: string[]; + }; + input_tokens: number; + output_tokens: number; +}; diff --git a/packages/global/core/dataset/type.d.ts b/packages/global/core/dataset/type.d.ts index 505b2fc8a2e6..1e1af70d9c9c 100644 --- a/packages/global/core/dataset/type.d.ts +++ b/packages/global/core/dataset/type.d.ts @@ -23,7 +23,7 @@ import type { import type { SourceMemberType } from 'support/user/type'; import type { DatasetDataIndexTypeEnum } from './data/constants'; import type { ParentIdType } from 'common/parentFolder/type'; - +import type { CollectionStatusEnum } from 'core/dataset/collection/schema'; export type ChunkSettingsType = { trainingType?: DatasetCollectionDataProcessModeEnum; @@ -75,47 +75,36 @@ export type ColumnSchemaType = { examples: string[]; forbid: boolean; valueIndex: boolean; - + // Database attributes isNullable?: boolean; - defaultValue?: string; + defaultValue?: string | null; isAutoIncrement?: boolean; isPrimaryKey?: boolean; isForeignKey?: boolean; relatedColumns?: string[]; - + // Extended metadata metadata?: Record; }; -export type ForeignKeySchemaType = { - constrainedColumns: string[]; - referredSchema: string | null; - referredTable: string; - referredColumns: string[]; -}; - -export type IndexSchemaType = { - name: string; - columns: string[]; - unique: boolean; - type: string; -}; - export type ConstraintSchemaType = { name: string; - type: string; // PRIMARY, FOREIGN, UNIQUE, CHECK - columns: string[]; - definition: string; + column: string; }; +export type ForeignKeySchemaType = ConstraintSchemaType & { + referredSchema: string; + referredTable: string; + referredColumns: string; +}; export type TableSchemaType = { tableName: string; description: string; + exist: boolean; columns: Record; foreignKeys: ForeignKeySchemaType[]; primaryKeys: string[]; - indexes: IndexSchemaType[]; constraints: ConstraintSchemaType[]; rowCount?: number; estimatedSize?: string; @@ -196,7 +185,7 @@ export type DatasetCollectionSchemaType = ChunkSettingsType & { // Parse settings customPdfParse?: boolean; trainingType: DatasetCollectionDataProcessModeEnum; - + // Database table schema (for database type collections) tableSchema?: TableSchemaType; }; diff --git a/packages/global/core/evaluation/api.d.ts b/packages/global/core/evaluation/api.d.ts index f2dfdb71ef28..91fd5f1e3401 100644 --- a/packages/global/core/evaluation/api.d.ts +++ b/packages/global/core/evaluation/api.d.ts @@ -8,6 +8,8 @@ import type { EvaluationDataItemType, EvaluationStatistics } from './type'; +import type { EvaluationStatusEnum } from './constants'; +import type { EvalDatasetDataKeyEnum } from './dataset/constants'; // ===== Common Types ===== export type MessageResponse = { message: string }; @@ -35,7 +37,6 @@ export type ListEvaluationsRequest = PaginationProps<{ searchKey?: string; appName?: string; appId?: string; - versionId?: string; }>; export type ListEvaluationsResponse = PaginationResponse; @@ -49,12 +50,8 @@ export type StopEvaluationResponse = MessageResponse; // Get Evaluation Stats export type StatsEvaluationRequest = EvalIdQuery; -export type EvaluationStatsResponse = { - total: number; - completed: number; - evaluating: number; - queuing: number; - error: number; +export type EvaluationStatsResponse = EvaluationStatistics & { + failed: number; }; // Export Evaluation Items @@ -74,18 +71,20 @@ export type RetryFailedItemsResponse = { export type EvalItemIdQuery = { evalItemId: string }; // List Evaluation Items -export type ListEvaluationItemsRequest = PaginationProps; +export type ListEvaluationItemsRequest = PaginationProps< + EvalIdQuery & { + status?: EvaluationStatusEnum; + belowThreshold?: boolean; + [EvalDatasetDataKeyEnum.UserInput]?: string; + [EvalDatasetDataKeyEnum.ExpectedOutput]?: string; + [EvalDatasetDataKeyEnum.ActualOutput]?: string; + } +>; export type ListEvaluationItemsResponse = PaginationResponse; // Get Evaluation Item Detail export type EvaluationItemDetailRequest = EvalItemIdQuery; -export type EvaluationItemDetailResponse = { - item: EvaluationItemSchemaType; - dataItem: any; - response?: string; - result?: any; - score?: number; -}; +export type EvaluationItemDetailResponse = EvaluationItemSchemaType; // Update Evaluation Item export type UpdateEvaluationItemRequest = EvalItemIdQuery & Partial; @@ -98,49 +97,3 @@ export type RetryEvaluationItemResponse = MessageResponse; // Delete Evaluation Item export type DeleteEvaluationItemRequest = EvalItemIdQuery; export type DeleteEvaluationItemResponse = MessageResponse; - -// ===== DataItem Aggregation API ===== - -// Query for dataItem ID -export type DataItemIdQuery = { dataItemId: string }; - -// DataItem List (Grouped by DataItem) -export type DataItemListRequest = PaginationProps< - EvalIdQuery & { - status?: number; // Optional: filter by status - keyword?: string; // Optional: search in dataItem content - } ->; -export type DataItemGroupedItem = { - dataItemId: string; - dataItem: EvaluationDataItemType; - items: EvaluationItemDisplayType[]; - statistics?: EvaluationStatistics; -}; -export type DataItemListResponse = PaginationResponse; - -// Delete DataItem Items -export type DeleteDataItemRequest = DataItemIdQuery & EvalIdQuery; -export type DeleteDataItemResponse = { - message: string; - deletedCount: number; -}; - -// Retry DataItem Items -export type RetryDataItemRequest = DataItemIdQuery & EvalIdQuery; -export type RetryDataItemResponse = { - message: string; - retriedCount: number; -}; - -// Update DataItem Items -export type UpdateDataItemRequest = DataItemIdQuery & EvalIdQuery & Partial; -export type UpdateDataItemResponse = { - message: string; - updatedCount: number; -}; - -// Export All DataItems Results -export type ExportDataItemsResultsRequest = EvalIdQuery & { - format?: 'csv' | 'json'; -}; diff --git a/packages/global/core/evaluation/constants.ts b/packages/global/core/evaluation/constants.ts index 0eed14daa109..ca240feaecf3 100644 --- a/packages/global/core/evaluation/constants.ts +++ b/packages/global/core/evaluation/constants.ts @@ -3,10 +3,10 @@ import { i18nT } from '../../../web/i18n/utils'; export const evaluationFileErrors = i18nT('dashboard_evaluation:eval_file_check_error'); export enum EvaluationStatusEnum { - queuing = 0, - evaluating = 1, - completed = 2, - error = 3 + queuing = 'queuing', + evaluating = 'evaluating', + completed = 'completed', + error = 'error' } export const EvaluationStatusMap = { @@ -23,13 +23,13 @@ export const EvaluationStatusMap = { name: i18nT('dashboard_evaluation:error') } }; -export const EvaluationStatusValues = Object.keys(EvaluationStatusMap).map(Number); +export const EvaluationStatusValues = Object.values(EvaluationStatusEnum); export enum SummaryStatusEnum { - pending = 0, - generating = 1, - completed = 2, - failed = 3 + pending = 'pending', + generating = 'generating', + completed = 'completed', + failed = 'failed' } export const SummaryStatusMap = { @@ -47,12 +47,12 @@ export const SummaryStatusMap = { } }; -export const SummaryStatusValues = Object.keys(SummaryStatusMap).map(Number); +export const SummaryStatusValues = Object.values(SummaryStatusEnum); // Calculation method enumeration export enum CalculateMethodEnum { - mean = 0, - median = 1 + mean = 'mean', + median = 'median' } export const CaculateMethodMap = { @@ -64,10 +64,10 @@ export const CaculateMethodMap = { } }; -export const CaculateMethodValues = Object.keys(CaculateMethodMap).map(Number); +export const CaculateMethodValues = Object.values(CalculateMethodEnum); // Score constants -export const PERFECT_SCORE = 100; +export const PERFECT_SCORE = 1; // Validation length constants export const MAX_NAME_LENGTH = 100; @@ -76,4 +76,3 @@ export const MAX_MODEL_NAME_LENGTH = 100; export const MAX_USER_INPUT_LENGTH = 1000; export const MAX_OUTPUT_LENGTH = 4000; export const MAX_PROMPT_LENGTH = 4000; -export const MAX_CSV_ROWS = 10000; diff --git a/packages/global/core/evaluation/dataset/api.d.ts b/packages/global/core/evaluation/dataset/api.d.ts index d6f1ee1dc86f..955cdc597beb 100644 --- a/packages/global/core/evaluation/dataset/api.d.ts +++ b/packages/global/core/evaluation/dataset/api.d.ts @@ -3,8 +3,11 @@ import type { EvalDatasetCollectionSchemaType, EvalDatasetDataSchemaType, EvalDatasetCollectionStatus, - EvalDatasetDataQualityStatus + EvalDatasetDataQualityStatus, + EvalDatasetDataQualityMetadata, + EvalDatasetDataSynthesisMetadata } from './type'; +import type { EvalDatasetDataQualityResultEnum } from './constants'; import type { EvalDatasetDataKeyEnum } from './constants'; type EvalDatasetCollectionBase = { @@ -39,14 +42,27 @@ export type listEvalDatasetCollectionResponse = PaginationResponse< dataItemsCount: number; } >; + +export type listEvalDatasetCollectionV2Body = { + searchKey?: string; + pageSize?: number; + pageNum?: number; + offset?: number; +}; + +export type listEvalDatasetCollectionV2Response = PaginationResponse< + Pick +>; type QualityEvaluationBase = { enableQualityEvaluation: boolean; evaluationModel?: string; }; export type importEvalDatasetFromFileBody = { - fileId: string; - collectionId: string; + collectionId?: string; // Optional - use existing collection mode + // Optional fields for creating new collection mode + name?: string; + description?: string; } & QualityEvaluationBase; type EvalDatasetDataBase = { [EvalDatasetDataKeyEnum.UserInput]: string; @@ -54,7 +70,9 @@ type EvalDatasetDataBase = { [EvalDatasetDataKeyEnum.ExpectedOutput]: string; [EvalDatasetDataKeyEnum.Context]?: string[]; [EvalDatasetDataKeyEnum.RetrievalContext]?: string[]; - [EvalDatasetDataKeyEnum.Metadata]?: Record; + qualityMetadata?: Partial; + synthesisMetadata?: Partial; + qualityResult?: EvalDatasetDataQualityResultEnum; }; export type createEvalDatasetDataBody = EvalDatasetDataBase & @@ -66,6 +84,7 @@ export type listEvalDatasetDataBody = PaginationProps<{ collectionId: string; searchKey?: string; status?: EvalDatasetDataQualityStatus; + qualityResult?: EvalDatasetDataQualityResultEnum; }>; export type listEvalDatasetDataResponse = PaginationResponse< @@ -77,7 +96,9 @@ export type listEvalDatasetDataResponse = PaginationResponse< | EvalDatasetDataKeyEnum.ExpectedOutput | EvalDatasetDataKeyEnum.Context | EvalDatasetDataKeyEnum.RetrievalContext - | 'metadata' + | 'qualityMetadata' + | 'synthesisMetadata' + | 'qualityResult' | 'createFrom' | 'createTime' | 'updateTime' @@ -119,23 +140,28 @@ export type getEvalDatasetDataDetailResponse = Pick< | '_id' | 'teamId' | 'tmbId' - | 'datasetId' + | 'evalDatasetCollectionId' | EvalDatasetDataKeyEnum.UserInput | EvalDatasetDataKeyEnum.ActualOutput | EvalDatasetDataKeyEnum.ExpectedOutput | EvalDatasetDataKeyEnum.Context | EvalDatasetDataKeyEnum.RetrievalContext - | EvalDatasetDataKeyEnum.Metadata + | 'qualityMetadata' + | 'synthesisMetadata' + | 'qualityResult' | 'createFrom' | 'createTime' | 'updateTime' >; export type smartGenerateEvalDatasetBody = { - collectionId: string; + collectionId?: string; kbDatasetIds: string[]; count?: number; intelligentGenerationModel: string; + // Optional fields for creating new collection + name?: string; + description?: string; }; export type listFailedTasksBody = { @@ -145,6 +171,7 @@ export type listFailedTasksBody = { export type listFailedTasksResponse = { tasks: Array<{ jobId: string; + // all about dataset dataId: string; datasetId: string; datasetName: string; diff --git a/packages/global/core/evaluation/dataset/constants.ts b/packages/global/core/evaluation/dataset/constants.ts index 2c7a5611d638..8418c21df8f9 100644 --- a/packages/global/core/evaluation/dataset/constants.ts +++ b/packages/global/core/evaluation/dataset/constants.ts @@ -18,7 +18,10 @@ export enum EvalDatasetDataQualityStatusEnum { queuing = 'queuing', evaluating = 'evaluating', error = 'error', - completed = 'completed', + completed = 'completed' +} + +export enum EvalDatasetDataQualityResultEnum { highQuality = 'highQuality', needsOptimization = 'needsOptimization' } @@ -33,3 +36,4 @@ export enum EvalDatasetDataKeyEnum { } export const EvalDatasetDataQualityStatusValues = Object.values(EvalDatasetDataQualityStatusEnum); +export const EvalDatasetDataQualityResultValues = Object.values(EvalDatasetDataQualityResultEnum); diff --git a/packages/global/core/evaluation/dataset/type.d.ts b/packages/global/core/evaluation/dataset/type.d.ts index e7dc5eaba0a2..5fc7189502e9 100644 --- a/packages/global/core/evaluation/dataset/type.d.ts +++ b/packages/global/core/evaluation/dataset/type.d.ts @@ -3,11 +3,36 @@ import type { EvalDatasetDataCreateFromEnum, EvalDatasetCollectionStatusEnum, EvalDatasetDataQualityStatusEnum, + EvalDatasetDataQualityResultEnum, EvalDatasetDataKeyEnum } from './constants'; +import type { Usage } from '@fastgpt/global/support/wallet/usage/type'; export type EvalDatasetCollectionStatus = EvalDatasetCollectionStatusEnum; export type EvalDatasetDataQualityStatus = EvalDatasetDataQualityStatusEnum; +export type EvalDatasetDataQualityResult = EvalDatasetDataQualityResultEnum; + +export type EvalDatasetDataQualityMetadata = { + status: EvalDatasetDataQualityStatusEnum; + score?: number; + reason?: string; + model?: string; + usages?: Usage[]; + runLogs?: any[]; + startTime?: Date; + finishTime?: Date; + queueTime?: Date; + error?: string; +}; + +export type EvalDatasetDataSynthesisMetadata = { + sourceDataId?: string; + sourceDatasetId?: string; + sourceCollectionId?: string; + intelligentGenerationModel?: string; + synthesizedAt?: Date; + generatedAt?: Date; +}; export type EvalDatasetCollectionSchemaType = { _id: string; @@ -25,13 +50,15 @@ export type EvalDatasetDataSchemaType = { _id: string; teamId: string; tmbId: string; - datasetId: string; + evalDatasetCollectionId: string; [EvalDatasetDataKeyEnum.UserInput]: string; [EvalDatasetDataKeyEnum.ActualOutput]: string; [EvalDatasetDataKeyEnum.ExpectedOutput]: string; [EvalDatasetDataKeyEnum.Context]: string[]; [EvalDatasetDataKeyEnum.RetrievalContext]: string[]; - metadata: Record; + qualityMetadata: EvalDatasetDataQualityMetadata; + synthesisMetadata?: EvalDatasetDataSynthesisMetadata; + qualityResult?: EvalDatasetDataQualityResultEnum; createFrom: EvalDatasetDataCreateFromEnum; createTime: Date; updateTime: Date; diff --git a/packages/global/core/evaluation/metric/constants.ts b/packages/global/core/evaluation/metric/constants.ts index 96a5889e348e..665fa8ad546e 100644 --- a/packages/global/core/evaluation/metric/constants.ts +++ b/packages/global/core/evaluation/metric/constants.ts @@ -10,8 +10,8 @@ export enum ModelTypeEnum { } export const ModelTypeValues = Object.values(ModelTypeEnum); -export enum EvaluationStatusEnum { +export enum MetricResultStatusEnum { Success = 'success', - Failed = 'failed', + Failed = 'failed' } -export const EvaluationStatusValues = Object.values(EvaluationStatusEnum); +export const MetricResultStatusValues = Object.values(MetricResultStatusEnum); diff --git a/packages/global/core/evaluation/metric/type.d.ts b/packages/global/core/evaluation/metric/type.d.ts index 3720ce1a2aed..5c54a8f1f209 100644 --- a/packages/global/core/evaluation/metric/type.d.ts +++ b/packages/global/core/evaluation/metric/type.d.ts @@ -1,5 +1,5 @@ import type { EvalDatasetDataKeyEnum } from '../dataset/constants'; -import type { ModelTypeEnum, EvalMetricTypeEnum, EvaluationStatusEnum } from './constants'; +import type { ModelTypeEnum, EvalMetricTypeEnum, MetricResultStatusEnum } from './constants'; import type { SourceMemberType } from '@fastgpt/global/support/user/type'; export type EvalModelConfigType = { @@ -20,7 +20,7 @@ export type EvalCase = { export type MetricResult = { metricName: string; - status: EvaluationStatusEnum; + status?: MetricResultStatusEnum; data?: EvaluationResult; usages?: Usage[]; error?: string; @@ -36,14 +36,14 @@ export type EvalMetricSchemaType = { type: EvalMetricTypeEnum; prompt?: string; - userInputRequired: boolean; - actualOutputRequired: boolean; - expectedOutputRequired: boolean; - contextRequired: boolean; - retrievalContextRequired: boolean; + userInputRequired?: boolean; + actualOutputRequired?: boolean; + expectedOutputRequired?: boolean; + contextRequired?: boolean; + retrievalContextRequired?: boolean; - embeddingRequired: boolean; - llmRequired: boolean; + embeddingRequired?: boolean; + llmRequired?: boolean; createTime: Date; updateTime: Date; @@ -86,7 +86,7 @@ export type Usage = { }; export type EvaluationResponse = { - status: EvaluationStatusEnum; + status: MetricResultStatusEnum; data?: EvaluationResult; usages?: Usage[]; error?: string; diff --git a/packages/global/core/evaluation/summary/api.d.ts b/packages/global/core/evaluation/summary/api.d.ts index c6d27d4e1e3e..24561e04f899 100644 --- a/packages/global/core/evaluation/summary/api.d.ts +++ b/packages/global/core/evaluation/summary/api.d.ts @@ -12,6 +12,7 @@ export interface MetricConfigItem { export interface MetricConfigItemWithName extends Omit { weight: number; // Required in config responses metricName: string; // Metric name for display + metricDescription: string; // Metric description for display } export interface UpdateMetricConfigItem extends Omit { @@ -60,6 +61,10 @@ export interface EvaluationSummaryResponse { errorReason?: string; completedItemCount: number; overThresholdItemCount: number; + underThresholdRate: number; + threshold: number; + weight: number; + customSummary: string; }>; aggregateScore: number; } diff --git a/packages/global/core/evaluation/type.d.ts b/packages/global/core/evaluation/type.d.ts index 5e00299a01ce..d4ae81d8639c 100644 --- a/packages/global/core/evaluation/type.d.ts +++ b/packages/global/core/evaluation/type.d.ts @@ -42,14 +42,16 @@ export interface EvaluatorSchema { metric: EvalMetricSchemaType; // Contains complete metric configuration runtimeConfig: RuntimeConfig; // Runtime configuration including LLM model thresholdValue?: number; - scoreScaling?: number; // Score scaling factor, default is 100 + scoreScaling?: number; // Score scaling factor, default is 1 } // Statistics information for evaluation task export interface EvaluationStatistics { - totalItems: number; - completedItems: number; - errorItems: number; + total: number; + completed: number; + evaluating: number; + queuing: number; + error: number; } // Improved evaluation task types @@ -59,12 +61,12 @@ export type EvaluationSchemaType = { tmbId: string; name: string; description?: string; - datasetId: string; // Associated dataset + evalDatasetCollectionId: string; // Associated evaluation dataset collection target: EvalTarget; // Embedded evaluation target evaluators: EvaluatorSchema[]; // Array of evaluator configurations summaryConfigs: SummaryConfig[]; // Array of summary configs, one for each metric usageId: string; - status: EvaluationStatusEnum; + status: EvaluationStatusEnum; // Computed real-time from job queues createTime: Date; finishTime?: Date; errorMessage?: string; @@ -94,28 +96,36 @@ export interface TargetCallParams { } /** - * Extended evaluation data item that combines dataset data with target call parameters. + * Evaluation data item that contains only the necessary fields for evaluation execution. * Used in evaluation context where both dataset content and execution parameters are needed. */ -export type EvaluationDataItemType = EvalDatasetDataSchemaType & { +export type EvaluationDataItemType = Pick< + EvalDatasetDataSchemaType, + | '_id' + | EvalDatasetDataKeyEnum.UserInput + | EvalDatasetDataKeyEnum.ExpectedOutput + | EvalDatasetDataKeyEnum.Context +> & { targetCallParams?: TargetCallParams; }; -// Evaluation item type (atomic: one dataItem + one target + one evaluator) +// Evaluation item type (batch: one dataItem + one target + multiple evaluators) export type EvaluationItemSchemaType = { _id: string; evalId: string; + // Chat information is stored in targetOutput.chatId and targetOutput.aiChatItemDataId // Dependent component configurations dataItem: EvaluationDataItemType; - target: EvalTarget; - evaluator: EvaluatorSchema; // Single evaluator configuration // Execution results targetOutput?: TargetOutput; // Actual output from target - evaluatorOutput?: MetricResult; // Result from single evaluator - status: EvaluationStatusEnum; - retry: number; + evaluatorOutputs?: MetricResult[]; // Results from multiple evaluators + status: EvaluationStatusEnum; // Computed real-time from job queues finishTime?: Date; errorMessage?: string; + // Metadata for optimization + metadata?: { + status: EvaluationStatusEnum; + }; }; // Evaluation target input/output types @@ -130,6 +140,8 @@ export interface TargetOutput { [EvalDatasetDataKeyEnum.RetrievalContext]?: string[]; usage?: any; responseTime: number; + chatId: string; + aiChatItemDataId: string; } export type EvaluationWithPerType = EvaluationSchemaType & { @@ -138,33 +150,34 @@ export type EvaluationWithPerType = EvaluationSchemaType & { // ===== Display Types ===== -export type EvaluationDisplayType = Pick< - EvaluationWithPerType, - | 'name' - | 'createTime' - | 'finishTime' - | 'status' - | 'errorMessage' - | 'tmbId' - | 'permission' - | 'statistics' -> & { - _id: string; - datasetName?: string; - target: EvalTarget; // Complete target object with extended config +// Extended SummaryConfig for display purposes (includes runtime calculated fields) +export interface SummaryConfigDisplay extends SummaryConfig { + score: number; // Real-time calculated metric score + completedItemCount: number; // Real-time calculated completed items count + overThresholdItemCount: number; // Real-time calculated over threshold items count +} + +export type EvaluationDisplayType = Omit & { + evalDatasetCollectionName?: string; metricNames: string[]; private: boolean; sourceMember: SourceMemberType; + summaryConfigs: SummaryConfigDisplay[]; // Use extended version for display + aggregateScore?: number; // Real-time calculated aggregate score }; export type EvaluationItemDisplayType = EvaluationItemSchemaType & { - evalItemId: string; + evaluators: Array<{ + metric: EvalMetricSchemaType; // Contains complete metric configuration + thresholdValue?: number; // Threshold value for this evaluator + weight?: number; + }>; // Array of evaluator configurations }; export interface CreateEvaluationParams { name: string; description?: string; - datasetId: string; + evalDatasetCollectionId: string; target: EvalTarget; // Only supports workflow type target configuration evaluators: EvaluatorSchema[]; // Replace metricIds with evaluators autoStart?: boolean; // Whether to automatically start the evaluation task after creation (default: true) diff --git a/packages/global/core/workflow/template/system/datasetSearch.ts b/packages/global/core/workflow/template/system/datasetSearch.ts index 85c8e85a1c97..8b0c87936399 100644 --- a/packages/global/core/workflow/template/system/datasetSearch.ts +++ b/packages/global/core/workflow/template/system/datasetSearch.ts @@ -134,6 +134,15 @@ export const DatasetSearchModule: FlowNodeTemplateType = { valueType: WorkflowIOValueTypeEnum.string, isPro: true, description: i18nT('workflow:filter_description') + }, + + // database + { + key: NodeInputKeyEnum.generateSqlModel, + renderTypeList: [FlowNodeInputTypeEnum.hidden], + label: i18nT('common:search_model'), + value: '', + valueType: WorkflowIOValueTypeEnum.string } ], outputs: [ diff --git a/packages/global/core/workflow/type/io.d.ts b/packages/global/core/workflow/type/io.d.ts index 62037dcb429d..95fa6e9a7acc 100644 --- a/packages/global/core/workflow/type/io.d.ts +++ b/packages/global/core/workflow/type/io.d.ts @@ -3,6 +3,7 @@ import type { LLMModelTypeEnum } from '../../ai/constants'; import type { WorkflowIOValueTypeEnum, NodeInputKeyEnum, NodeOutputKeyEnum } from '../constants'; import type { FlowNodeInputTypeEnum, FlowNodeOutputTypeEnum } from '../node/constant'; import type { SecretValueType } from '../../../common/secret/type'; +import type { DatasetTypeEnum } from '@fastgpt/global/core/dataset/constants'; // Dynamic input field configuration export type CustomFieldConfigType = { @@ -112,7 +113,8 @@ export type SelectedDatasetType = { avatar: string; name: string; vectorModel: EmbeddingModelItemType; - dataCount?: number + dataCount?: number; + datasetType?: `${DatasetTypeEnum}`; }[]; /* http node */ diff --git a/packages/service/common/bullmq/index.ts b/packages/service/common/bullmq/index.ts index c41f62fb774e..fc96c4ea4dd7 100644 --- a/packages/service/common/bullmq/index.ts +++ b/packages/service/common/bullmq/index.ts @@ -21,10 +21,10 @@ const defaultWorkerOpts: Omit = { export enum QueueNames { datasetSync = 'datasetSync', evalDatasetDataQuality = 'evalDatasetDataQuality', - evalDatasetSmartGenerate = 'evalDatasetSmartGenerate', evalDatasetDataSynthesize = 'evalDatasetDataSynthesize', evalTask = 'evalTask', evalTaskItem = 'evalTaskitem', + evaluationSummary = 'evaluationSummary', // abondoned websiteSync = 'websiteSync' } diff --git a/packages/service/common/file/multer.ts b/packages/service/common/file/multer.ts index 237407656654..a2f77f2592cf 100644 --- a/packages/service/common/file/multer.ts +++ b/packages/service/common/file/multer.ts @@ -2,9 +2,19 @@ import type { NextApiRequest, NextApiResponse } from 'next'; import multer from 'multer'; import path from 'path'; import type { BucketNameEnum } from '@fastgpt/global/common/file/constants'; -import { bucketNameMap } from '@fastgpt/global/common/file/constants'; +import { + bucketNameMap, + DEFAULT_FILE_UPLOAD_LIMITS, + FileUploadErrorEnum +} from '@fastgpt/global/common/file/constants'; import { getNanoid } from '@fastgpt/global/common/string/tools'; -import { UserError } from '@fastgpt/global/common/error/utils'; +import { UserError, FileUploadError } from '@fastgpt/global/common/error/utils'; +import { + validateFileUpload, + getEffectiveUploadLimits, + formatUploadLimitsMessage, + type FileUploadLimits +} from './uploadValidation'; export type FileType = { fieldname: string; @@ -18,14 +28,24 @@ export type FileType = { /* maxSize: File max size (MB) + customLimits: Custom upload limits configuration */ -export const getUploadModel = ({ maxSize = 500 }: { maxSize?: number }) => { - maxSize *= 1024 * 1024; +export const getUploadModel = ({ + maxSize = DEFAULT_FILE_UPLOAD_LIMITS.MAX_FILE_SIZE_MB, + customLimits +}: { + maxSize?: number; + customLimits?: FileUploadLimits; +} = {}) => { + const limits = getEffectiveUploadLimits(customLimits); + const effectiveMaxSize = Math.min(maxSize, limits.maxFileSizeMB); + const maxSizeBytes = effectiveMaxSize * 1024 * 1024; class UploadModel { uploaderSingle = multer({ limits: { - fieldSize: maxSize + fileSize: maxSizeBytes, + fieldSize: maxSizeBytes }, preservePath: true, storage: multer.diskStorage({ @@ -56,6 +76,15 @@ export const getUploadModel = ({ maxSize = 500 }: { maxSize?: number }) => { // @ts-ignore this.uploaderSingle(req, res, (error) => { if (error) { + if (error.code === 'LIMIT_FILE_SIZE') { + return reject( + new FileUploadError( + FileUploadErrorEnum.FILE_TOO_LARGE, + `File exceeds the maximum file size limit of ${effectiveMaxSize}MB`, + { maxSizeMB: effectiveMaxSize } + ) + ); + } return reject(error); } @@ -68,11 +97,23 @@ export const getUploadModel = ({ maxSize = 500 }: { maxSize?: number }) => { // @ts-ignore const file = req.file as FileType; + if (!file) { + return reject(new UserError('No file uploaded')); + } + + // Validate single file + const decodedFile = { + ...file, + originalname: decodeURIComponent(file.originalname) + }; + + const validation = validateFileUpload([decodedFile], customLimits); + if (!validation.isValid) { + return reject(validation.errors[0]); + } + resolve({ - file: { - ...file, - originalname: decodeURIComponent(file.originalname) - }, + file: decodedFile, bucketName, metadata: (() => { if (!req.body?.metadata) return {}; @@ -97,7 +138,9 @@ export const getUploadModel = ({ maxSize = 500 }: { maxSize?: number }) => { uploaderMultiple = multer({ limits: { - fieldSize: maxSize + fileSize: maxSizeBytes, + files: limits.maxFileCount, + fieldSize: maxSizeBytes }, preservePath: true, storage: multer.diskStorage({ @@ -113,7 +156,7 @@ export const getUploadModel = ({ maxSize = 500 }: { maxSize?: number }) => { } } }) - }).array('file', global.feConfigs?.uploadFileMaxSize); + }).array('file', limits.maxFileCount); async getUploadFiles(req: NextApiRequest, res: NextApiResponse) { return new Promise<{ files: FileType[]; @@ -122,18 +165,67 @@ export const getUploadModel = ({ maxSize = 500 }: { maxSize?: number }) => { // @ts-ignore this.uploaderMultiple(req, res, (error) => { if (error) { - console.log(error); + console.log('File upload error:', error); + + if (error.code === 'LIMIT_FILE_SIZE') { + return reject( + new FileUploadError( + FileUploadErrorEnum.FILE_TOO_LARGE, + `File exceeds the maximum file size limit of ${effectiveMaxSize}MB`, + { maxSizeMB: effectiveMaxSize } + ) + ); + } + if (error.code === 'LIMIT_FILE_COUNT') { + return reject( + new FileUploadError( + FileUploadErrorEnum.TOO_MANY_FILES, + `File count exceeds the limit, maximum ${limits.maxFileCount} files supported`, + { maxFileCount: limits.maxFileCount } + ) + ); + } + if (error.code === 'LIMIT_UNEXPECTED_FILE') { + return reject( + new FileUploadError( + FileUploadErrorEnum.TOO_MANY_FILES, + `File count exceeds the limit, maximum ${limits.maxFileCount} files supported`, + { maxFileCount: limits.maxFileCount } + ) + ); + } + return reject(error); } // @ts-ignore const files = req.files as FileType[]; + if (!files || files.length === 0) { + return reject(new UserError('No files uploaded')); + } + + // Decode filenames and validate all files + const decodedFiles = files.map((file) => ({ + ...file, + originalname: decodeURIComponent(file.originalname) + })); + + const validation = validateFileUpload(decodedFiles, customLimits); + if (!validation.isValid) { + // Return the first error, but include information about all errors + const errorMessage = validation.errors.map((err) => err.message).join('; '); + return reject( + new FileUploadError(validation.errors[0].code, errorMessage, { + errors: validation.errors, + totalErrors: validation.errors.length, + limits: formatUploadLimitsMessage(limits) + }) + ); + } + resolve({ - files: files.map((file) => ({ - ...file, - originalname: decodeURIComponent(file.originalname) - })), + files: validation.validFiles, data: (() => { if (!req.body?.data) return {}; try { diff --git a/packages/service/common/file/read/utils.ts b/packages/service/common/file/read/utils.ts index 461da09dc0c4..437123f8cd5f 100644 --- a/packages/service/common/file/read/utils.ts +++ b/packages/service/common/file/read/utils.ts @@ -73,6 +73,8 @@ export const readRawContentByFileBuffer = async ({ const token = global.systemEnv.customPdfParse?.key; if (!url) return systemParse(); + const timeout = global.systemEnv.customPdfParse?.timeout || 10; + const start = Date.now(); addLog.info('Parsing files from an external service'); @@ -85,7 +87,7 @@ export const readRawContentByFileBuffer = async ({ markdown: string; error?: Object | string; }>(url, data, { - timeout: 600000, + timeout: timeout * 1000 * 60, headers: { ...data.getHeaders(), Authorization: token ? `Bearer ${token}` : undefined diff --git a/packages/service/common/file/uploadValidation.ts b/packages/service/common/file/uploadValidation.ts new file mode 100644 index 000000000000..a6b8a263d824 --- /dev/null +++ b/packages/service/common/file/uploadValidation.ts @@ -0,0 +1,183 @@ +import { FileUploadError } from '@fastgpt/global/common/error/utils'; +import { + DEFAULT_FILE_UPLOAD_LIMITS, + FileUploadErrorEnum +} from '@fastgpt/global/common/file/constants'; +import type { FileType } from './multer'; + +export interface FileUploadLimits { + maxFileCount?: number; + maxFileSizeMB?: number; + allowedTypes?: string[]; +} + +export interface FileValidationResult { + isValid: boolean; + errors: FileUploadError[]; + validFiles: FileType[]; + invalidFiles: FileType[]; +} + +/** + * Get effective upload limits by merging global config with custom limits + */ +export function getEffectiveUploadLimits( + customLimits?: FileUploadLimits +): Required { + const globalLimits = { + maxFileCount: + global.feConfigs?.uploadFileMaxAmount ?? DEFAULT_FILE_UPLOAD_LIMITS.MAX_FILE_COUNT, + maxFileSizeMB: + global.feConfigs?.uploadFileMaxSize ?? DEFAULT_FILE_UPLOAD_LIMITS.MAX_FILE_SIZE_MB + }; + + return { + maxFileCount: Math.min( + customLimits?.maxFileCount ?? globalLimits.maxFileCount, + globalLimits.maxFileCount + ), + maxFileSizeMB: Math.min( + customLimits?.maxFileSizeMB ?? globalLimits.maxFileSizeMB, + globalLimits.maxFileSizeMB + ), + allowedTypes: customLimits?.allowedTypes ?? [] + }; +} + +/** + * Validate file size constraints + */ +export function validateFileSize( + file: FileType, + limits: Required +): FileUploadError | null { + const maxSizeBytes = limits.maxFileSizeMB * 1024 * 1024; + + if (file.size > maxSizeBytes) { + return new FileUploadError( + FileUploadErrorEnum.FILE_TOO_LARGE, + `File "${file.originalname}" exceeds the maximum file size limit of ${limits.maxFileSizeMB}MB`, + { + fileName: file.originalname, + fileSize: file.size, + maxSize: maxSizeBytes, + maxSizeMB: limits.maxFileSizeMB + } + ); + } + + return null; +} + +/** + * Validate file count constraints + */ +export function validateFileCount( + files: FileType[], + limits: Required +): FileUploadError | null { + if (files.length > limits.maxFileCount) { + return new FileUploadError( + FileUploadErrorEnum.TOO_MANY_FILES, + `File count exceeds the limit, maximum ${limits.maxFileCount} files supported`, + { + fileCount: files.length, + maxFileCount: limits.maxFileCount + } + ); + } + + return null; +} + +/** + * Validate file type constraints + */ +export function validateFileType( + file: FileType, + limits: Required +): FileUploadError | null { + if (limits.allowedTypes.length === 0) { + return null; // No type restrictions + } + + const fileExtension = file.originalname + .toLowerCase() + .substring(file.originalname.lastIndexOf('.')); + const isAllowed = limits.allowedTypes.some( + (type) => type.toLowerCase() === fileExtension || file.mimetype.includes(type.toLowerCase()) + ); + + if (!isAllowed) { + return new FileUploadError( + FileUploadErrorEnum.INVALID_FILE_TYPE, + `File type not supported, file "${file.originalname}" has type ${fileExtension}`, + { + fileName: file.originalname, + fileType: fileExtension, + mimetype: file.mimetype, + allowedTypes: limits.allowedTypes + } + ); + } + + return null; +} + +export function validateFileUpload( + files: FileType[], + customLimits?: FileUploadLimits +): FileValidationResult { + const limits = getEffectiveUploadLimits(customLimits); + const errors: FileUploadError[] = []; + const validFiles: FileType[] = []; + const invalidFiles: FileType[] = []; + + const countError = validateFileCount(files, limits); + if (countError) { + errors.push(countError); + return { + isValid: false, + errors, + validFiles: [], + invalidFiles: files + }; + } + + for (const file of files) { + const fileErrors: FileUploadError[] = []; + + const sizeError = validateFileSize(file, limits); + if (sizeError) fileErrors.push(sizeError); + + const typeError = validateFileType(file, limits); + if (typeError) fileErrors.push(typeError); + + if (fileErrors.length > 0) { + errors.push(...fileErrors); + invalidFiles.push(file); + } else { + validFiles.push(file); + } + } + + return { + isValid: errors.length === 0, + errors, + validFiles, + invalidFiles + }; +} + +export function formatUploadLimitsMessage(limits: Required): string { + const messages = [ + `Maximum ${limits.maxFileCount} files supported`, + `Maximum file size ${limits.maxFileSizeMB}MB` + ]; + + if (limits.allowedTypes.length > 0) { + messages.push(`Supported file types: ${limits.allowedTypes.join(', ')}`); + } + + return messages.join(', '); +} diff --git a/packages/service/common/redis/index.ts b/packages/service/common/redis/index.ts index 55f8126bafb7..ab634bec101c 100644 --- a/packages/service/common/redis/index.ts +++ b/packages/service/common/redis/index.ts @@ -4,7 +4,9 @@ import Redis from 'ioredis'; const REDIS_URL = process.env.REDIS_URL ?? 'redis://localhost:6379'; export const newQueueRedisConnection = () => { - const redis = new Redis(REDIS_URL); + const redis = new Redis(REDIS_URL, { + maxRetriesPerRequest: null + }); redis.on('connect', () => { console.log('Redis connected'); }); diff --git a/packages/service/common/vectorDB/constants.ts b/packages/service/common/vectorDB/constants.ts index 804ff60a1656..9338c3fa994d 100644 --- a/packages/service/common/vectorDB/constants.ts +++ b/packages/service/common/vectorDB/constants.ts @@ -1,7 +1,7 @@ export const DatasetVectorDbName = 'fastgpt'; export const DatasetVectorTableName = 'modeldata'; -export const DBDatasetVectorTableName = 'CoulumnDescriptionIndex'; -export const DBDatasetValueVectorTableName = 'CoulumnValueIndex'; +export const DBDatasetVectorTableName = 'coulumndescriptionindex'; +export const DBDatasetValueVectorTableName = 'coulumnvlueindex'; export const PG_ADDRESS = process.env.PG_URL; export const OCEANBASE_ADDRESS = process.env.OCEANBASE_URL; export const MILVUS_ADDRESS = process.env.MILVUS_ADDRESS; diff --git a/packages/service/common/vectorDB/pg/index.ts b/packages/service/common/vectorDB/pg/index.ts index ef53ecf44cda..79eb353f6929 100644 --- a/packages/service/common/vectorDB/pg/index.ts +++ b/packages/service/common/vectorDB/pg/index.ts @@ -41,7 +41,7 @@ export class PgVectorCtrl { vector VECTOR(1536) NOT NULL, dataset_id VARCHAR(50) NOT NULL, collection_id VARCHAR(50) NOT NULL, - column_des_index VARCHAR(100) NOT NULL, + column_des_index VARCHAR(1024) NOT NULL, team_id VARCHAR(50) NOT NULL, createtime TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); @@ -56,7 +56,7 @@ export class PgVectorCtrl { team_id VARCHAR(50) NOT NULL, dataset_id VARCHAR(50) NOT NULL, collection_id VARCHAR(50) NOT NULL, - column_val_index VARCHAR(100) NOT NULL, + column_val_index VARCHAR(1024) NOT NULL, createtime TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); `); @@ -79,7 +79,7 @@ export class PgVectorCtrl { `CREATE INDEX CONCURRENTLY IF NOT EXISTS team_dataset_collection_index ON ${DBDatasetVectorTableName} USING btree(team_id, dataset_id, collection_id);` ); await PgClient.query( - `CREATE INDEX CONCURRENTLY IF NOT EXISTS table_des_team_index ON ${DBDatasetVectorTableName} USING btree(team_id);` + `CREATE INDEX CONCURRENTLY IF NOT EXISTS table_des_create_time_index ON ${DBDatasetVectorTableName} USING btree(createtime);` ); // ColumnValueIndex @@ -125,9 +125,7 @@ export class PgVectorCtrl { datasetId, collectionId, vectors, - retry = 3, tableName = DatasetVectorTableName, - table_des_index, column_des_index, column_val_index } = props; @@ -271,7 +269,7 @@ export class PgVectorCtrl { } = props; let index: string = ''; if (tableName == DBDatasetVectorTableName) index = 'column_des_index'; - if (tableName == DBDatasetValueVectorTableName) index = 'column_value_index'; + if (tableName == DBDatasetValueVectorTableName) index = 'column_val_index'; try { // Build forbid collection filter @@ -286,8 +284,8 @@ export class PgVectorCtrl { SET LOCAL hnsw.iterative_scan = relaxed_order; WITH relaxed_results AS MATERIALIZED ( SELECT id, collection_id, - ${tableName === DBDatasetVectorTableName ? 'column_des_index' : 'column_val_index'} as index_field, - vector <#> '[${vector}]' AS score + ${index} as index_field, + vector <=> '[${vector}]' AS score FROM ${tableName} WHERE team_id = '${teamId}' AND dataset_id IN (${datasetIds.map((id: string) => `'${String(id)}'`).join(',')}) diff --git a/packages/service/core/app/controller.ts b/packages/service/core/app/controller.ts index b476606f5b1c..03b5b1e56d77 100644 --- a/packages/service/core/app/controller.ts +++ b/packages/service/core/app/controller.ts @@ -1,6 +1,7 @@ import { type AppSchema } from '@fastgpt/global/core/app/type'; import { NodeInputKeyEnum } from '@fastgpt/global/core/workflow/constants'; import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant'; +import type { DatasetTypeEnum } from '@fastgpt/global/core/dataset/constants'; import { AppTypeEnum } from '@fastgpt/global/core/app/constants'; import { MongoApp } from './schema'; import type { StoreNodeItemType } from '@fastgpt/global/core/workflow/type/node'; @@ -55,13 +56,15 @@ export const beforeUpdateAppFormat = ({ nodes }: { nodes?: StoreNodeItemType[] } return; } input.value = val - .map((dataset: { datasetId: string }) => ({ + .map((dataset: any & { datasetType?: DatasetTypeEnum }) => ({ + ...dataset, datasetId: dataset.datasetId })) .filter((item) => !!item.datasetId); } else if (typeof val === 'object' && val !== null) { input.value = [ { + ...(val as any & { datasetType?: DatasetTypeEnum }), datasetId: val.datasetId } ]; diff --git a/packages/service/core/app/utils.ts b/packages/service/core/app/utils.ts index 70ffaea5c198..f447634f728e 100644 --- a/packages/service/core/app/utils.ts +++ b/packages/service/core/app/utils.ts @@ -2,6 +2,7 @@ import { MongoDataset } from '../dataset/schema'; import { getEmbeddingModel } from '../ai/model'; import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant'; import { NodeInputKeyEnum } from '@fastgpt/global/core/workflow/constants'; +import type { DatasetTypeEnum } from '@fastgpt/global/core/dataset/constants'; import type { StoreNodeItemType } from '@fastgpt/global/core/workflow/type/node'; import { getChildAppPreviewNode } from './plugin/controller'; import { PluginSourceEnum } from '@fastgpt/global/core/app/plugin/constants'; @@ -155,7 +156,10 @@ export async function rewriteAppWorkflowToDetail({ node.inputs.forEach((item) => { if (item.key !== NodeInputKeyEnum.datasetSelectList) return; - const val = item.value as undefined | { datasetId: string }[] | { datasetId: string }; + const val = item.value as + | undefined + | (any & { datasetType?: DatasetTypeEnum })[] + | (any & { datasetType?: DatasetTypeEnum }); if (Array.isArray(val)) { item.value = val @@ -172,7 +176,8 @@ export async function rewriteAppWorkflowToDetail({ datasetId: data.datasetId, avatar: data.avatar, name: data.name, - vectorModel: data.vectorModel + vectorModel: data.vectorModel, + datasetType: v.datasetType }; }) .filter(Boolean); @@ -193,7 +198,8 @@ export async function rewriteAppWorkflowToDetail({ datasetId: data.datasetId, avatar: data.avatar, name: data.name, - vectorModel: data.vectorModel + vectorModel: data.vectorModel, + datasetType: val.datasetType } ]; } diff --git a/packages/service/core/dataset/collection/controller.ts b/packages/service/core/dataset/collection/controller.ts index 34fdbfe5ef9f..5b4fe06f1f58 100644 --- a/packages/service/core/dataset/collection/controller.ts +++ b/packages/service/core/dataset/collection/controller.ts @@ -37,7 +37,11 @@ import { } from '@fastgpt/global/core/dataset/training/utils'; import { DatasetDataIndexTypeEnum } from '@fastgpt/global/core/dataset/data/constants'; import { clearCollectionImages, removeDatasetImageExpiredTime } from '../image/utils'; -import { DBDatasetVectorTableName,DBDatasetValueVectorTableName,DatasetVectorTableName } from '../../../common/vectorDB/constants'; +import { + DBDatasetVectorTableName, + DBDatasetValueVectorTableName, + DatasetVectorTableName +} from '../../../common/vectorDB/constants'; export const createCollectionAndInsertData = async ({ dataset, rawText, @@ -275,7 +279,8 @@ export async function createOneCollection({ session, ...props }: CreateOneCollec externalFileUrl, apiFileId, apiFileParentId, - tableSchema + tableSchema, + forbid } = props; const collectionTags = await createOrGetCollectionTags({ @@ -302,7 +307,8 @@ export async function createOneCollection({ session, ...props }: CreateOneCollec ...(externalFileUrl ? { externalFileUrl } : {}), ...(apiFileId ? { apiFileId } : {}), ...(apiFileParentId ? { apiFileParentId } : {}), - ...(tableSchema ? { tableSchema } : {}) + ...(tableSchema ? { tableSchema } : {}), + forbid: forbid ?? false } ], { session, ordered: true } @@ -417,9 +423,24 @@ export async function delCollection({ ] : []), // Delete vector data - deleteDatasetDataVector({ teamId, datasetIds, collectionIds ,tableName:DatasetVectorTableName}), - deleteDatasetDataVector({ teamId, datasetIds, collectionIds ,tableName:DBDatasetVectorTableName}), - deleteDatasetDataVector({ teamId, datasetIds, collectionIds ,tableName:DBDatasetValueVectorTableName}), + deleteDatasetDataVector({ + teamId, + datasetIds, + collectionIds, + tableName: DatasetVectorTableName + }), + deleteDatasetDataVector({ + teamId, + datasetIds, + collectionIds, + tableName: DBDatasetVectorTableName + }), + deleteDatasetDataVector({ + teamId, + datasetIds, + collectionIds, + tableName: DBDatasetValueVectorTableName + }) ]); // delete collections diff --git a/packages/service/core/dataset/collection/schema.ts b/packages/service/core/dataset/collection/schema.ts index c5993b6519a1..814577c9a83c 100644 --- a/packages/service/core/dataset/collection/schema.ts +++ b/packages/service/core/dataset/collection/schema.ts @@ -11,46 +11,47 @@ import { export const DatasetColCollectionName = 'dataset_collections'; // Column Schema for database tables -const ColumnSchema = new Schema({ - columnName: { type: String, required: true }, - columnType: { type: String, default: 'TEXT' }, - description: { type: String, default: '' }, - examples: { type: [String], default: [] }, - forbid: { type: Boolean, default: false }, - valueIndex: { type: Boolean, default: true }, - - // Database attributes - isPrimaryKey: { type: Boolean, default: false }, - isForeignKey: { type: Boolean, default: false }, - relatedColumns: { type: [String], default: [] }, - - // Extended metadata - metadata: { type: Object, default: {} } -}, { _id: false }); - -// Foreign Key Schema -const ForeignKeySchema = new Schema({ - constrainedColumns: { type: [String], default: [] }, - referredSchema: { type: String, default: null }, - referredTable: { type: String, required: true }, - referredColumns: { type: [String], default: [] } -}, { _id: false }); - -// Index Schema -const IndexSchema = new Schema({ - name: { type: String, required: true }, - columns: { type: [String], default: [] }, - unique: { type: Boolean, default: false }, - type: { type: String, default: 'BTREE' } -}, { _id: false }); +const ColumnSchema = new Schema( + { + columnName: { type: String, required: true }, + columnType: { type: String, default: 'TEXT' }, + description: { type: String, default: '' }, + examples: { type: [String], default: [] }, + forbid: { type: Boolean, default: false }, + valueIndex: { type: Boolean, default: true }, + + // Database attributes + isNullable: { type: Boolean, default: true }, + defaultValue: { type: String, default: null }, + isAutoIncrement: { type: Boolean, default: false }, + isPrimaryKey: { type: Boolean, default: false }, + isForeignKey: { type: Boolean, default: false }, + relatedColumns: { type: [String], default: [] }, + + // Extended metadata + metadata: { type: Object, default: {} } + }, + { _id: false } +); // Constraint Schema -const ConstraintSchema = new Schema({ - name: { type: String, required: true }, - type: { type: String, required: true }, // PRIMARY, FOREIGN, UNIQUE, CHECK - columns: { type: [String], default: [] }, - definition: { type: String, default: '' } -}, { _id: false }); +const ConstraintSchema = new Schema( + { + name: { type: String, required: true }, + column: { type: String, default: '' } + }, + { _id: false } +); + +// Foreign Key Schema +const ForeignKeySchema = new Schema( + { + referredSchema: { type: String }, + referredTable: { type: String, required: true }, + referredColumns: { type: String, required: true } + }, + { _id: false } +).add(ConstraintSchema); const DatasetCollectionSchema = new Schema({ parentId: { @@ -97,18 +98,18 @@ const DatasetCollectionSchema = new Schema({ type: Date, default: () => new Date() }, - tableSchema: { + tableSchema: { type: { tableName: { type: String, required: true }, description: { type: String, default: '' }, - columns: { + exist: { type: Boolean, default: true }, + columns: { type: Map, of: ColumnSchema, default: {} }, foreignKeys: { type: [ForeignKeySchema], default: [] }, primaryKeys: { type: [String], default: [] }, - indexes: { type: [IndexSchema], default: [] }, constraints: { type: [ConstraintSchema], default: [] }, rowCount: Number, estimatedSize: String, diff --git a/packages/service/core/dataset/database/clientManager.ts b/packages/service/core/dataset/database/clientManager.ts index 1419c00b696b..b4b842bcfc8a 100644 --- a/packages/service/core/dataset/database/clientManager.ts +++ b/packages/service/core/dataset/database/clientManager.ts @@ -2,8 +2,9 @@ import type { DatabaseConfig } from '@fastgpt/global/core/dataset/type'; import { DatabaseErrEnum } from '@fastgpt/global/common/error/code/database'; import { addLog } from '../../../common/system/log'; import { MysqlClient } from './model/mysql'; -import type { AsyncDB } from './model/AsyncDB'; +import type { AsyncDB } from './model/asyncDB'; import { MongoDataset } from '../schema'; +import { i18nT } from '../../../../web/i18n/utils'; export async function createDatabaseClient(config: DatabaseConfig): Promise { switch (config.client) { @@ -14,15 +15,13 @@ export async function createDatabaseClient(config: DatabaseConfig): Promise { +export async function checkDatabaseConnection(config: DatabaseConfig): Promise { let dbClient: AsyncDB | undefined; try { dbClient = await createDatabaseClient(config); const result = await dbClient.checkConnection(); return result; } catch (err: any) { - addLog.error(`Database connection test failed`, err); - console.log('testDatabaseConnection', err, typeof err); return Promise.reject(err); } finally { if (dbClient) { @@ -43,17 +42,17 @@ export async function withDatabaseClient( try { const dataset = await MongoDataset.findById(datasetId); if (!dataset?.databaseConfig) { - return Promise.reject('数据库未配置'); + return Promise.reject(DatabaseErrEnum.dbConfigNotFound); } dbClient = await createDatabaseClient(dataset.databaseConfig); - + await dbClient.checkConnection(); const result = await operation(dbClient); return result; } catch (err) { - addLog.error(`Database operation failed for dataset ${datasetId}`, err); - throw err; + addLog.error(`Database operation failed`, err); + return Promise.reject(i18nT('database_client:op_unknown_database_error')); } finally { if (dbClient) { try { @@ -75,12 +74,13 @@ export async function withDatabaseClientByConfig( try { dbClient = await createDatabaseClient(config); + await dbClient.checkConnection(); const result = await operation(dbClient); return result; } catch (err) { addLog.error(`Database operation failed`, err); - throw err; + return Promise.reject(i18nT('database_client:op_unknown_database_error')); } finally { if (dbClient) { try { diff --git a/packages/service/core/dataset/database/model/AsyncDB.ts b/packages/service/core/dataset/database/model/AsyncDB.ts deleted file mode 100644 index f2287864fce0..000000000000 --- a/packages/service/core/dataset/database/model/AsyncDB.ts +++ /dev/null @@ -1,460 +0,0 @@ -import {DatabaseType} from '@fastgpt/global/core/dataset/constants'; -import { DatabaseErrEnum } from '@fastgpt/global/common/error/code/database'; -import type {DatabaseConfig} from '@fastgpt/global/core/dataset/type'; -import {DBTable, TableColumn, TableKeyInfo, TableForeignKey, TableIndex, TableConstraint} from './dataModel' -import {truncateText, isStringType, convertValueToString} from "./utils";; -import type {TableColumn as ORMColumn, ColumnType, DataSourceOptions, Driver} from "typeorm"; -import {DataSource, Entity} from "typeorm"; - -export abstract class AsyncDB { - protected db: DataSource; - protected config: DatabaseConfig; - protected sample_value_num: number; - protected sql_result_limit: number; - protected max_string_length: number; - protected db_server_info: string; - protected table_names: Array - - constructor( - db: DataSource, - config: DatabaseConfig, - sample_value_num: number = 3, - sql_result_limit: number = 100, - max_string_length: number = 300 - ) { - this.db = db; - this.config = config; - this.sample_value_num = sample_value_num; - this.sql_result_limit = sql_result_limit; - this.max_string_length = max_string_length; - this.db_server_info = ''; - this.table_names = new Array() - } - - static fromConfig(config: DatabaseConfig): AsyncDB { - throw new Error(DatabaseErrEnum.notImplemented) - } - - static from_uri(config: DatabaseConfig): DataSource { - const options: DataSourceOptions = { - type: config.client as any, // 'mysql' | 'postgres' | 'sqlite' - host: config.host, - port: config.port, - username: config.user, - password: config.password, - database: config.database, - synchronize: false, - logging: false - }; - console.debug(`[AsyncDB.from_uri]:${Object.values(options)}`) - return new DataSource(options); - } - - async checkConnection(): Promise { - try { - if (!this.db.isInitialized) { - await this.db.initialize(); - } - await this.db.query("SELECT 1"); - return Promise.resolve(true); - } catch (err: any) { - return Promise.reject(err) - } - } - - async destroy(): Promise { - try { - if (this.db.isInitialized) { - await this.db.destroy(); - } - // @ts-ignore - this.db = null; - } catch (err: any) { - return Promise.reject(DatabaseErrEnum.clientDestroyError) - } - } - - async dialect(): Promise { - return new Promise((resolve, reject) => { - if (!this.db.isInitialized) { - reject('client does not Initialized'); - return; - } - resolve(this.db.options.type); - }); - } - - public driver(): Promise { - return new Promise((resolve, reject) => { - if (!this.db.isInitialized) { - reject('client does not Initialized'); - return; - } - resolve(this.db.driver); - }); - } - - async get_db_server_info(): Promise { - if (!this.db.isInitialized) { - await this.db.initialize(); - } - - const dbDriver = await this.driver(); - return `${dbDriver.options.type}-${dbDriver.version || 'unknown'}`; - } - - /*-----------------------Dynamic Introspection Methods-----------------------*/ - async introspect_database(): Promise> { - if (!this.db.isInitialized) { - await this.db.initialize(); - } - - const queryRunner = this.db.createQueryRunner(); - const tables = new Map(); - - try { - const tableMetadatas = await queryRunner.getTables(await this.get_all_table_names()); - for (const tableMetadata of tableMetadatas) { - const tableName = tableMetadata.name; - - - const columns = new Map(); - - for (const column of tableMetadata.columns) { - - const tableColumn = new TableColumn( - column.name, - column.type as ColumnType, - column.comment || '', - true, - true, - [] - ); - - // 添加列的详细属性 - // tableColumn.forbid = !column.isGenerated; - - columns.set(column.name, tableColumn); - } - - // 获取主键 - const primaryKeys = tableMetadata.primaryColumns.map(col => col.name); - - // 获取外键 - const foreignKeys: TableForeignKey[] = tableMetadata.foreignKeys.map(fk => { - const referencedSchema = fk.referencedTableName.includes('.') - ? fk.referencedTableName.split('.')[0] - : null; - const referencedTable = fk.referencedTableName.includes('.') - ? fk.referencedTableName.split('.')[1] - : fk.referencedTableName; - - return new TableForeignKey( - fk.columnNames, - referencedSchema, - referencedTable, - fk.referencedColumnNames - ); - }); - - // 获取索引信息 - const indexes: TableIndex[] = tableMetadata.indices.map(index => { - return new TableIndex( - index.name || `idx_${tableName}_${index.columnNames.join('_')}`, - index.columnNames, - index.isUnique, - false, // 主键索引单独处理 - 'btree' // 默认类型 - ); - }); - - // 添加主键索引 - if (primaryKeys.length > 0) { - indexes.push(new TableIndex( - `pk_${tableName}`, - primaryKeys, - true, - true, - 'btree' - )); - } - - // 获取约束信息 - const constraints: TableConstraint[] = []; - - // 添加主键约束 - if (primaryKeys.length > 0) { - constraints.push(new TableConstraint( - `pk_${tableName}`, - 'primary_key', - primaryKeys - )); - } - - // 添加外键约束 - foreignKeys.forEach(fk => { - constraints.push(new TableConstraint( - `fk_${tableName}_${fk.constrained_columns.join('_')}`, - 'foreign_key', - fk.constrained_columns - )); - }); - - // 添加唯一约束 - tableMetadata.uniques.forEach(unique => { - constraints.push(new TableConstraint( - unique.name || `uk_${tableName}_${unique.columnNames.join('_')}`, - 'unique', - unique.columnNames - )); - }); - - // 创建表对象 - const dbTable = new DBTable( - tableName, - tableMetadata.comment || '', - false, - columns, - foreignKeys, - primaryKeys, - indexes, - constraints - ); - - tables.set(tableName, dbTable); - } - - return tables; - - } finally { - await queryRunner.release(); - } - } - - async get_database_statistics(): Promise { - if (!this.db.isInitialized) { - await this.db.initialize(); - } - - const queryRunner = this.db.createQueryRunner(); - - try { - const stats: any = { - tableCount: 0, - totalColumns: 0, - foreignKeyCount: 0, - indexCount: 0, - tableStats: [] - }; - - const tables = await this.introspect_database(); - stats.tableCount = tables.size; - - for (const tableName of tables.keys()) { - const table = tables.get(tableName)!; - stats.totalColumns += table.columns.size; - stats.foreignKeyCount += table.foreign_keys.length; - - try { - if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(tableName)) { - await Promise.reject(DatabaseErrEnum.invalidTableName); - } - const countResult = await queryRunner.query( - `SELECT COUNT(*) as count - FROM ${this.getProtectedTableName(tableName)}` - ); - const rowCount = countResult[0]?.count || 0; - - stats.tableStats.push({ - tableName, - columnCount: table.columns.size, - rowCount: parseInt(rowCount), - foreignKeyCount: table.foreign_keys.length, - primaryKeyCount: table.primary_keys.length - }); - } catch (error) { - console.warn(`无法获取表 ${tableName} 的行数:`, error); - } - } - - return stats; - - } finally { - await queryRunner.release(); - } - } - - - async get_all_table_names(): Promise> { - if (!this.db.isInitialized) { - console.log('Initializing database connection...'); - await this.db.initialize(); - console.log('Database connection initialized'); - } - const queryRunner = this.db.createQueryRunner(); - try { - // 获取当前数据库名 - const dbName = this.db.options.database; - - // 查询所有表名 - const tables: { TABLE_NAME: string }[] = await queryRunner.query( - `SELECT TABLE_NAME - FROM information_schema.tables - WHERE table_schema = ? - AND TABLE_TYPE = 'BASE TABLE'`, - [dbName] - ); - - return tables.map(t => t.TABLE_NAME); - } finally { - await queryRunner.release(); - } -} - - async init_db_schema(): Promise { - this.db_server_info = await this.get_db_server_info() - this.table_names = await this.get_all_table_names() - } - - async get_table_columns(table_name: string): Promise> { - const queryRunner = this.db.createQueryRunner(); - try { - const table = await queryRunner.getTable(table_name); - if (!table) throw new Error(`Table ${table_name} not found`); - - return table.columns.map((col: ORMColumn) => { - return new TableColumn( - col.name, - col.type as ColumnType, // 直接使用TypeORM的原生类型 - col.comment ?? "" - ); - }); - } finally { - await queryRunner.release(); - } - } - - - protected getProtectedTableName(tableName: string): string { - switch (this.config.client) { - case DatabaseType.mysql: - case DatabaseType.sqlite: - return `\`${tableName}\``; - case DatabaseType.postgresql: - return `"${tableName}"`; - default: - return tableName; - } - } - - - protected getProtectedColName(columnName: string): string { - switch (this.config.client) { - case DatabaseType.mysql: - case DatabaseType.sqlite: - return `\`${columnName}\``; - case DatabaseType.postgresql: - return `"${columnName}"`; - default: - return columnName; - } - } - - - async get_table_info(tableName: string, getExamples: boolean = false): Promise { - if (!this.db.isInitialized) { - await this.db.initialize(); - } - - const queryRunner = this.db.createQueryRunner(); - - try { - const table = await queryRunner.getTable(tableName); - if (!table) return Promise.reject(DatabaseErrEnum.fetchInfoError); - - const tableComment = table.comment ?? ""; - - const columns = new Map(); - - for (const col of table.columns) { - let examples: string[] = []; - let valueIndex = false; - - if (getExamples) { - const sql = ` - SELECT DISTINCT ${this.getProtectedColName(col.name)} - FROM ${this.getProtectedTableName(tableName)} - WHERE ${this.getProtectedColName(col.name)} IS NOT NULL - LIMIT ${this.sample_value_num + 1} - `; - - try { - const result = await queryRunner.query(sql); - - const rawExamples: any[] = []; - - for (const row of result) { - const value = row[col.name]; - if (value !== null && value !== undefined) { - const strValue = truncateText( - convertValueToString(value), - this.max_string_length - ); - rawExamples.push(strValue); - } - } - - if (isStringType(col.type as ColumnType) && rawExamples.length > 0) valueIndex = true; - examples = rawExamples.slice(0, this.sample_value_num); - - } catch (error) { - console.warn(`获取列 ${col.name} 的示例数据失败:`, error); - examples = []; - } - } - - const tableColumn = new TableColumn( - col.name, - col.type as ColumnType, - col.comment || "", - false, - valueIndex, - examples - ); - - columns.set(col.name, tableColumn); - } - - const primaryKeys = table.primaryColumns.map(col => col.name); - - const foreignKeys: TableForeignKey[] = table.foreignKeys.map(fk => { - return new TableForeignKey( - fk.columnNames, - fk.referencedTableName.includes(".") ? fk.referencedTableName.split(".")[0] : null, - fk.referencedTableName.includes(".") ? fk.referencedTableName.split(".")[1] : fk.referencedTableName, - fk.referencedColumnNames - ); - }); - - return new DBTable( - tableName, - tableComment, - false, - columns, - foreignKeys, - primaryKeys, - [], // indexes - [] // constraints - ); - - } finally { - await queryRunner.release(); - } - } - - async aget_table_info(tableName: string, getExamples: boolean = false): Promise { - return this.get_table_info(tableName, getExamples); - } - - -} diff --git a/packages/service/core/dataset/database/model/asyncDB.ts b/packages/service/core/dataset/database/model/asyncDB.ts new file mode 100644 index 000000000000..a926d4927ebd --- /dev/null +++ b/packages/service/core/dataset/database/model/asyncDB.ts @@ -0,0 +1,262 @@ +import { DatabaseType } from '@fastgpt/global/core/dataset/constants'; +import { DatabaseErrEnum } from '@fastgpt/global/common/error/code/database'; +import type { DatabaseConfig } from '@fastgpt/global/core/dataset/type'; +import { DBTable, TableColumn, TableForeignKey } from './dataModel'; +import { truncateText, isStringType, convertValueToString } from './utils'; +import type { TableColumn as ORMColumn, ColumnType, DataSourceOptions, Driver } from 'typeorm'; +import { DataSource } from 'typeorm'; + +export abstract class AsyncDB { + protected db: DataSource; + protected config: DatabaseConfig; + protected sample_value_num: number; + protected sql_result_limit: number; + protected max_string_length: number; + protected db_server_info: string; + protected table_names: Array; + + constructor( + db: DataSource, + config: DatabaseConfig, + sample_value_num: number = 3, + sql_result_limit: number = 100, + max_string_length: number = 1024 + ) { + this.db = db; + this.config = config; + this.sample_value_num = sample_value_num; + this.sql_result_limit = sql_result_limit; + this.max_string_length = max_string_length; + this.db_server_info = ''; + this.table_names = new Array(); + } + + static fromConfig(config: DatabaseConfig): AsyncDB { + throw new Error(DatabaseErrEnum.notImplemented); + } + + static from_uri(config: DatabaseConfig): DataSource { + const options: DataSourceOptions = { + type: config.client as any, // 'mysql' | 'postgres' | 'sqlite' + host: config.host, + port: config.port, + username: config.user, + password: config.password, + database: config.database, + synchronize: false, + logging: false + }; + console.debug(`[AsyncDB.from_uri]:${Object.values(options)}`); + return new DataSource(options); + } + + async checkConnection(): Promise { + try { + if (!this.db.isInitialized) { + await this.db.initialize(); + } + await this.db.query('SELECT 1'); + return Promise.resolve(true); + } catch (err: any) { + return Promise.reject(err); + } + } + + async destroy(): Promise { + try { + if (this.db.isInitialized) { + await this.db.destroy(); + } + // @ts-ignore + this.db = null; + } catch (err: any) { + return Promise.reject(DatabaseErrEnum.clientDestroyError); + } + } + + async dialect(): Promise { + return new Promise((resolve, reject) => { + if (!this.db.isInitialized) { + reject(DatabaseErrEnum.clientNotFound); + return; + } + resolve(this.db.options.type); + }); + } + + public driver(): Promise { + return new Promise((resolve, reject) => { + if (!this.db.isInitialized) { + reject(DatabaseErrEnum.clientNotFound); + return; + } + resolve(this.db.driver); + }); + } + + async get_db_server_info(): Promise { + if (!this.db.isInitialized) { + await this.db.initialize(); + } + + const dbDriver = await this.driver(); + return `${dbDriver.options.type}-${dbDriver.version || 'unknown'}`; + } + + /*-----------------------Dynamic Introspection Methods-----------------------*/ + async get_all_table_names(): Promise> { + if (!this.db.isInitialized) { + await this.db.initialize(); + } + const queryRunner = this.db.createQueryRunner(); + // try-finally to ensure the queryRunner is released + try { + // 获取当前数据库名 + const dbName = this.db.options.database; + + // 查询所有表名 + const tables: { TABLE_NAME: string }[] = await queryRunner.query( + `SELECT TABLE_NAME + FROM information_schema.tables + WHERE table_schema = ? + AND TABLE_TYPE = 'BASE TABLE'`, + [dbName] + ); + + return tables.map((t) => t.TABLE_NAME); + } finally { + await queryRunner.release(); + } + } + + async init_db_schema(): Promise { + this.db_server_info = await this.get_db_server_info(); + this.table_names = await this.get_all_table_names(); + } + + async get_table_columns(table_name: string): Promise> { + const queryRunner = this.db.createQueryRunner(); + try { + const table = await queryRunner.getTable(table_name); + if (!table) return Promise.reject(DatabaseErrEnum.fetchInfoError); + + return table.columns.map((col: ORMColumn) => { + return new TableColumn(col.name, col.type as ColumnType, col.comment ?? ''); + }); + } finally { + await queryRunner.release(); + } + } + + protected getProtectedTableName(tableName: string): string { + switch (this.config.client) { + case DatabaseType.mysql: + case DatabaseType.sqlite: + return `\`${tableName}\``; + case DatabaseType.postgresql: + return `"${tableName}"`; + default: + return tableName; + } + } + + protected getProtectedColName(columnName: string): string { + switch (this.config.client) { + case DatabaseType.mysql: + case DatabaseType.sqlite: + return `\`${columnName}\``; + case DatabaseType.postgresql: + return `"${columnName}"`; + default: + return columnName; + } + } + + async aget_table_info(tableName: string, getExamples: boolean = false): Promise { + if (!this.db.isInitialized) { + await this.db.initialize(); + } + + const queryRunner = this.db.createQueryRunner(); + // try-finally to ensure the queryRunner is released + try { + const table = await queryRunner.getTable(tableName); + if (!table) return Promise.reject(DatabaseErrEnum.fetchInfoError); + + const tableComment = table.comment ?? ''; + + const columns = new Map(); + + for (const col of table.columns) { + let examples: string[] = []; + let valueIndex = false; + + if (getExamples) { + const sql = ` + SELECT DISTINCT ${this.getProtectedColName(col.name)} + FROM ${this.getProtectedTableName(tableName)} + WHERE ${this.getProtectedColName(col.name)} IS NOT NULL + LIMIT ${this.sample_value_num + 1} + `; + + try { + const result = await queryRunner.query(sql); + + const rawExamples: any[] = []; + + for (const row of result) { + const value = row[col.name]; + if (value !== null && value !== undefined) { + const strValue = truncateText(convertValueToString(value), this.max_string_length); + rawExamples.push(strValue); + } + } + + if (isStringType(col.type as ColumnType) && rawExamples.length > 0) valueIndex = true; + examples = rawExamples.slice(0, this.sample_value_num); + } catch (error) { + console.warn(`获取列 ${col.name} 的示例数据失败:`, error); + examples = []; + } + } + + const tableColumn = new TableColumn( + col.name, + col.type as ColumnType, + col.comment || '', + false, + valueIndex, + examples, + col.isNullable, + col.default ?? null, + col.isGenerated, + col.isPrimary, + table.foreignKeys.some((fk) => fk.columnNames.includes(col.name)), + table.foreignKeys + ?.filter((fk) => fk.columnNames.includes(col.name)) + .map((fk) => fk.referencedColumnNames) + .flat() + ); + columns.set(col.name, tableColumn); + } + + const primaryKeys = table.primaryColumns.map((col) => col.name); + const foreignKeys: TableForeignKey[] = table.foreignKeys.flatMap((fk) => { + return fk.columnNames.map( + (col, idx) => + new TableForeignKey( + fk.name || '', + col, + fk.referencedSchema || fk.referencedDatabase || this.config.database, + fk.referencedTableName, + fk.referencedColumnNames[idx] + ) + ); + }); + + return new DBTable(tableName, tableComment, false, columns, foreignKeys, primaryKeys); + } finally { + await queryRunner.release(); + } + } +} diff --git a/packages/service/core/dataset/database/model/dataModel.ts b/packages/service/core/dataset/database/model/dataModel.ts index e06357b4b64c..5d415daaeeee 100644 --- a/packages/service/core/dataset/database/model/dataModel.ts +++ b/packages/service/core/dataset/database/model/dataModel.ts @@ -1,246 +1,201 @@ -import type {ValueTransformer, DataSource, ColumnType} from "typeorm"; +import type { ColumnType } from 'typeorm'; +import { truncateText } from './utils'; +import type { DatabaseCollectionsTable } from '@fastgpt/global/core/dataset/database/api'; +import type { ColumnSchemaType } from '@fastgpt/global/core/dataset/type'; -export class RequestValidationDiagnosisError extends Error { -} +export class RequestValidationDiagnosisError extends Error {} export class TableColumn { - public columnName: string; - public columnType: ColumnType; - private _description: string = ""; - public examples: Array; - public forbid: boolean; - public value_index: boolean; - - constructor( - columnName: string, - columnType: ColumnType, - description: string = "", - forbid: boolean = true, - value_index: boolean = true, - examples: Array = [], - ) { - this.columnName = columnName; - this.columnType = columnType; - this.description = description; // 会触发 setter 校验 - this.examples = examples; - this.forbid = forbid; - this.value_index = value_index; - } - - set description(value: string) { - if (value.length > 1024) { - throw new Error("字段描述长度不能超过1024个字符."); - } - this._description = value; - } - - get description(): string { - return this._description; - } + columnName: string; + columnType: ColumnType; + description: string; + examples: Array; + forbid: boolean; + valueIndex: boolean; + + // Database attributes + isNullable?: boolean; + defaultValue?: string | null; + isAutoIncrement?: boolean; + isPrimaryKey?: boolean; + isForeignKey?: boolean; + relatedColumns?: string[]; + + constructor( + columnName: string, + columnType: ColumnType, + description: string = '', + forbid: boolean = true, + value_index: boolean = true, + examples: Array = [], + isNullable: boolean = true, + defaultValue?: string | null, + isAutoIncrement: boolean = false, + isPrimaryKey: boolean = false, + isForeignKey: boolean = false, + relatedColumns?: string[] + ) { + this.columnName = columnName; + this.columnType = columnType; + this.description = description; // 会触发 setter 校验 + this.examples = examples; + this.forbid = forbid; + this.valueIndex = value_index; + // Database constraints + this.isNullable = isNullable; + this.defaultValue = defaultValue; + this.isAutoIncrement = isAutoIncrement; + this.isPrimaryKey = isPrimaryKey; + this.isForeignKey = isForeignKey; + this.relatedColumns = relatedColumns; + } } +export class TableConstraint { + name: string; // constraint name + column: string; // constrained column -export class TableColumnTransformer implements ValueTransformer { - - to(entityValue: TableColumn | null): any { - if (!entityValue) return null; - return { - columnName: entityValue.columnName, - columnType: entityValue.columnType, - description: entityValue.description, - examples: entityValue.examples, - forbid: entityValue.forbid, - value_index: entityValue.value_index, - }; - } - - - from(databaseValue: any): TableColumn | null { - if (!databaseValue) return null; - return new TableColumn( - databaseValue.columnName, - databaseValue.columnType, - databaseValue.description, - databaseValue.forbid, - databaseValue.value_index, - databaseValue.examples - ); - } -} - -export class TableForeignKey { - constrained_columns: Array - referred_schema: string | null - referred_table: string - referred_columns: Array - - constructor( - constrained_columns: Array, - referred_schema: string | null, - referred_table: string, - referred_columns: Array - ) { - this.constrained_columns = constrained_columns; - this.referred_schema = referred_schema - this.referred_table = referred_table - this.referred_columns = referred_columns - } -} - -export class TableIndex { - name: string - columns: Array - isUnique: boolean - isPrimary: boolean - type: string - - constructor( - name: string, - columns: Array, - isUnique: boolean = false, - isPrimary: boolean = false, - type: string = 'btree' - ) { - this.name = name; - this.columns = columns; - this.isUnique = isUnique; - this.isPrimary = isPrimary; - this.type = type; - } + constructor(name: string, column: string) { + this.name = name; + this.column = column; + } } -export class TableConstraint { - name: string - type: 'unique' | 'check' | 'foreign_key' | 'primary_key' - columns: Array - definition?: string - - constructor( - name: string, - type: 'unique' | 'check' | 'foreign_key' | 'primary_key', - columns: Array, - definition?: string - ) { - this.name = name; - this.type = type; - this.columns = columns; - this.definition = definition; - } +export class TableForeignKey extends TableConstraint { + referredSchema: string; + referredTable: string; + referredColumns: string; + constructor( + name: string, // constraint name + column: string, // constrained column + referredSchema: string, + referredTable: string, + referredColumns: string + ) { + super(name, column); + this.referredSchema = referredSchema; + this.referredTable = referredTable; + this.referredColumns = referredColumns; + } } export class TableKeyInfo { - columns: Map; - foreign_keys: Array; - primary_keys: Array; - - constructor( - columns: Map, - foreign_keys: Array, - primary_keys: Array - ) { - this.columns = columns; - this.foreign_keys = foreign_keys; - this.primary_keys = primary_keys; - } + columns: Map; + foreignKeys: Array; + primaryKeys: Array; + + constructor( + columns: Map, + foreignKeys: Array, + primaryKeys: Array + ) { + this.columns = columns; + this.foreignKeys = foreignKeys; + this.primaryKeys = primaryKeys; + } } export class DBTable extends TableKeyInfo { - private _name: string = ""; - private _description: string = ""; - forbid: boolean; - indexes: Array; - constraints: Array; - rowCount?: number; - estimatedSize?: string; - - constructor( - name: string, - description: string = "", - forbid: boolean = true, - columns: Map, - foreign_keys: Array, - primary_keys: Array, - indexes: Array = [], - constraints: Array = [] - ) { - super(columns, foreign_keys, primary_keys) - this.name = name - this.description = description - this.forbid = forbid - this.indexes = indexes; - this.constraints = constraints; - } - - set name(value: string) { - if (!value) { - throw new RequestValidationDiagnosisError("表名不能为空.") - } - if (value.length > 100) { - throw new RequestValidationDiagnosisError("表名长度不能超过100个字符.") - } - this._name = value - } - - get name() { - return this._name - } - - set description(value: string) { - if (value.length > 1024) { - throw new Error("字段描述长度不能超过1024个字符."); - } - this._description = value - } - - get description() { - return this._description - } + tableName: string; + description: string; + forbid: boolean; + constraints: Array; + rowCount?: number; + estimatedSize?: string; + + constructor( + tableName: string, + description: string, + forbid: boolean = true, + columns: Map, + foreignKeys: Array, + primaryKeys: Array, + constraints: Array = [] + ) { + super(columns, foreignKeys, primaryKeys); + this.tableName = tableName; + this.description = description; + this.forbid = forbid; + this.constraints = constraints; + } } +export class TableColumnTransformer { + /** + * Convert TableColumn Object to plain object + * @param tableColumn TableColumn Object + * @returns plain object + */ + static toPlainObject(tableColumn: TableColumn): any { + if (!tableColumn) return null; + console.debug('[TableColumnTransformer toPlainObject] tableColumn', tableColumn.defaultValue); + return { + columnName: tableColumn.columnName, + columnType: String(tableColumn.columnType), + description: tableColumn.description, + examples: tableColumn.examples, + forbid: tableColumn.forbid, + valueIndex: tableColumn.valueIndex, + // Database attributes + isNullable: tableColumn.isNullable, + defaultValue: tableColumn.defaultValue, + isAutoIncrement: tableColumn.isAutoIncrement, + isPrimaryKey: tableColumn.isPrimaryKey, + isForeignKey: tableColumn.isForeignKey, + relatedColumns: tableColumn.relatedColumns + }; + } + + static fromPlainObject(col: ColumnSchemaType): TableColumn { + return new TableColumn( + col.columnName, + col.columnType as ColumnType, + col.description, + col.forbid, + col.valueIndex, + col.examples, + col.isNullable, + col.defaultValue, + col.isAutoIncrement, + col.isPrimaryKey, + col.isForeignKey, + col.relatedColumns + ); + } +} -export class DBIntrospector { - constructor(private readonly dataSource: DataSource) {} - - async aget_table_info( - tableName: string, - getExamples: boolean = false - ): Promise { - const metadata = this.dataSource.getMetadata(tableName); - - // 表注释 - const tableComment = metadata.tableMetadataArgs?.comment ?? ""; - - // 收集字段 - const columns = new Map(); - for (const col of metadata.columns) { - const name = col.propertyName; - const type = col.type as ColumnType; - const comment = col.comment ?? ""; - - columns.set(name, new TableColumn(name, type, comment)); - } - - // 主键 - const primaryKeys = metadata.primaryColumns.map((col) => col.propertyName); - - // 外键 - const foreignKeys: TableForeignKey[] = metadata.foreignKeys.map((fk) => - new TableForeignKey( - fk.columns.map((c) => c.propertyName), - fk.referencedEntityMetadata.schema ?? null, - fk.referencedEntityMetadata.tableName, - fk.referencedColumns.map((c) => c.propertyName), - ) - ); - - - return new DBTable( - tableName, - tableComment, - false, - columns, - foreignKeys, - primaryKeys - ); - } -} \ No newline at end of file +export class TableTransformer { + static toPlainObject(table: DBTable, extra: Record = {}): any { + const columnObj: Record = {}; + table.columns.forEach((value, key) => { + columnObj[key] = TableColumnTransformer.toPlainObject(value); + }); + + return { + tableName: table.tableName, + description: table.description, + columns: columnObj, + foreignKeys: table.foreignKeys, + primaryKeys: table.primaryKeys, + constraints: table.constraints, + ...extra + }; + } + + static fromPlainObject(table: DatabaseCollectionsTable): DBTable { + return new DBTable( + table.tableName, + table.description, + table.forbid, + new Map( + Object.entries(table.columns).map(([key, value]) => [ + key, + TableColumnTransformer.fromPlainObject(value) + ]) + ), + table.foreignKeys, + table.primaryKeys, + table.constraints + ); + } +} diff --git a/packages/service/core/dataset/database/model/mysql.ts b/packages/service/core/dataset/database/model/mysql.ts index c02c9226f930..3c39a4ea57fa 100644 --- a/packages/service/core/dataset/database/model/mysql.ts +++ b/packages/service/core/dataset/database/model/mysql.ts @@ -1,33 +1,48 @@ -import type {DatabaseConfig} from '@fastgpt/global/core/dataset/type' -import { AsyncDB } from './AsyncDB' +import type { DatabaseConfig } from '@fastgpt/global/core/dataset/type'; +import { AsyncDB } from './asyncDB'; import { DatabaseErrEnum } from '@fastgpt/global/common/error/code/database'; +import { addLog } from '../../../../common/system/log'; export class MysqlClient extends AsyncDB { - static fromConfig(config: DatabaseConfig): MysqlClient { - const db = AsyncDB.from_uri(config); - return new MysqlClient(db, config); + const db = AsyncDB.from_uri(config); + return new MysqlClient(db, config); } override async checkConnection(): Promise { - try { - await super.checkConnection(); - return true; - } catch (err: any) { - if (err?.code === "ER_ACCESS_DENIED_ERROR") { - // username or password error - return Promise.reject(DatabaseErrEnum.authError); - } else if (err?.code === "ER_BAD_DB_ERROR") { - // database not found - return Promise.reject(DatabaseErrEnum.clientNotFound); - } else if (err?.code === "PROTOCOL_CONNECTION_LOST" || err?.code === "ENOTFOUND" || err?.code === "ETIMEDOUT") { - // url error - return Promise.reject(DatabaseErrEnum.connectionFailed); - } else { - // other - return Promise.reject(DatabaseErrEnum.checkError); - } + try { + await super.checkConnection(); + return true; + } catch (err: any) { + addLog.warn('[checkConnection]:', err); + if (err?.code === 'ER_ACCESS_DENIED_ERROR') { + // username or password error + return Promise.reject(DatabaseErrEnum.authError); + } else if (err?.code === 'ER_BAD_DB_ERROR') { + // database not found + return Promise.reject(DatabaseErrEnum.databaseNameError); + } else if (err?.code === 'PROTOCOL_CONNECTION_LOST') { + // connection lost + return Promise.reject(DatabaseErrEnum.connectionLost); + } else if (err?.code === 'ECONNREFUSED') { + // Error: Connection Refused + return Promise.reject(DatabaseErrEnum.econnRefused); + } else if (err?.code === 'ETIMEDOUT') { + // timeout error + return Promise.reject(DatabaseErrEnum.connectionTimeout); + } else if (err?.code === 'ENOTFOUND') { + // url error + return Promise.reject(DatabaseErrEnum.connectionFailed); + } else if (err?.code === 'EHOSTUNREACH') { + // address error + return Promise.reject(DatabaseErrEnum.hostError); + } else if (err?.code === 'ERR_SOCKET_BAD_PORT') { + // port error + return Promise.reject(DatabaseErrEnum.databasePortError); + } else { + // other + return Promise.reject(DatabaseErrEnum.checkError); } + } } - } diff --git a/packages/service/core/dataset/schema.ts b/packages/service/core/dataset/schema.ts index f36603b8d396..93016ac9d5fb 100644 --- a/packages/service/core/dataset/schema.ts +++ b/packages/service/core/dataset/schema.ts @@ -101,7 +101,7 @@ const DatasetSchema = new Schema({ }, agentModel: { type: String, - required: function(this: any) { + required: function (this: any) { return this.type !== DatasetTypeEnum.database; }, default: 'gpt-4o-mini' @@ -125,13 +125,13 @@ const DatasetSchema = new Schema({ }, databaseConfig: { type: { - client:{ + client: { type: String, required: true }, - version:{ + version: { type: String, - default: "5.7.44" + default: '5.7.44' }, host: { type: String, @@ -153,10 +153,10 @@ const DatasetSchema = new Schema({ type: String, required: true }, - encrypt:{ + encrypt: { type: Boolean, - default: true - }, + default: false + }, poolSize: { type: Number, default: 20 diff --git a/packages/service/core/dataset/search/controller.ts b/packages/service/core/dataset/search/controller.ts index 368fa294325c..de2c586de1c2 100644 --- a/packages/service/core/dataset/search/controller.ts +++ b/packages/service/core/dataset/search/controller.ts @@ -3,7 +3,10 @@ import { DatasetSearchModeMap, SearchScoreTypeEnum } from '@fastgpt/global/core/dataset/constants'; -import { recallFromVectorStore,databaseEmbeddingRecall } from '../../../common/vectorDB/controller'; +import { + recallFromVectorStore, + databaseEmbeddingRecall +} from '../../../common/vectorDB/controller'; import { getVectorsByText } from '../../ai/embedding'; import { getEmbeddingModel, getDefaultRerankModel, getLLMModel } from '../../ai/model'; import { MongoDatasetData } from '../data/schema'; @@ -32,9 +35,22 @@ import type { NodeInputKeyEnum } from '@fastgpt/global/core/workflow/constants'; import { datasetSearchQueryExtension } from './utils'; import type { RerankModelItemType } from '@fastgpt/global/core/ai/model.d'; import { formatDatasetDataValue } from '../data/controller'; -import { DBDatasetValueVectorTableName, DBDatasetVectorTableName } from '../../../common/vectorDB/constants'; +import { + DBDatasetValueVectorTableName, + DBDatasetVectorTableName +} from '../../../common/vectorDB/constants'; import { MongoDataset } from '../schema'; import { addLog } from '../../../common/system/log'; +import { DatasetErrEnum } from '@fastgpt/global/common/error/code/dataset'; +import { i18nT } from '../../../../web/i18n/utils'; +import { DatabaseErrEnum } from '@fastgpt/global/common/error/code/database'; +import type { + DativeForeignKey, + DativeTable, + DativeTableColumns, + SqlGenerationRequest, + SqlGenerationResponse +} from '@fastgpt/global/core/dataset/database/api'; export type SearchDatasetDataProps = { histories: ChatItemType[]; @@ -97,11 +113,14 @@ export type SearchDatasetDataResponse = { }; export type SearchDatabaseDataResponse = { - schema: Record; + schema: Record< + string, + { + collectionId: string; + datasetId: string; + score: number; + } + >; tokens: number; }; @@ -1006,18 +1025,10 @@ export const deepRagSearch = (data: DeepRagSearchProps) => global.deepRagHandler * @returns DatabaseEmbedRecallResult containing schema mapping and token usage */ export const SearchDatabaseData = async ( - props:SearchDatabaseDataProps + props: SearchDatabaseDataProps ): Promise => { - let { - histories, - teamId, - model, - datasetIds, - queries, - limit: maxTokens - } = props; + let { histories, teamId, model, datasetIds, queries, limit = 50 } = props; try { - // Get forbid collection list for database search const forbidCollections = await MongoDatasetCollection.find( { @@ -1036,8 +1047,8 @@ export const SearchDatabaseData = async ( const forbidCollectionIdList = forbidCollections.map((item: any) => String(item._id)); await Promise.all( - queries.map(async (query:string) => { - const {tokens, vectors} = await getVectorsByText({ + queries.map(async (query: string) => { + const { tokens, vectors } = await getVectorsByText({ model: vectorModel, input: query, type: 'query' @@ -1051,7 +1062,7 @@ export const SearchDatabaseData = async ( teamId, datasetIds, vector: q_vector, - limit: maxTokens, + limit: limit, forbidCollectionIdList }); @@ -1060,14 +1071,14 @@ export const SearchDatabaseData = async ( teamId, datasetIds, vector: q_vector, - limit: maxTokens, + limit: limit, forbidCollectionIdList }); - columnDescriptionRecallResList.push(...columnDescriptionResults) - columnValueRecallResultList.push(...columnValueResults) + columnDescriptionRecallResList.push(...columnDescriptionResults); + columnValueRecallResultList.push(...columnValueResults); }) - ) + ); // Step 5: Merge and integrate results const schema = await mergeAndGetSchema({ @@ -1076,7 +1087,7 @@ export const SearchDatabaseData = async ( teamId }); - addLog.info(`Database embed recall completed. Found ${Object.keys(schema).length} tables.`); + addLog.debug(`Database embed recall completed. Found ${Object.keys(schema).length} tables.`); addLog.debug('Schema results:', schema); return { @@ -1084,11 +1095,7 @@ export const SearchDatabaseData = async ( tokens: totalTokens }; } catch (error) { - addLog.error('Database embed recall error', error); - return { - schema: {}, - tokens: 0 - }; + return Promise.reject(i18nT('chat:embedding_model_error')); } }; @@ -1180,7 +1187,7 @@ const mergeAndGetSchema = async ({ const collectionIds = new Set(); // Collect all collection IDs from both results - [...columnDescriptionRecallResList, ...columnValueRecallResultList].forEach(result => { + [...columnDescriptionRecallResList, ...columnValueRecallResultList].forEach((result) => { if (result.collectionId) { collectionIds.add(result.collectionId); } @@ -1194,7 +1201,7 @@ const mergeAndGetSchema = async ({ .select('_id datasetId name') .lean(); - const collectionMap = new Map(collections.map(col => [String(col._id), col])); + const collectionMap = new Map(collections.map((coll) => [String(coll._id), coll])); // Process column description results for (const result of columnDescriptionRecallResList) { @@ -1237,54 +1244,9 @@ const mergeAndGetSchema = async ({ return schema; }; - -// SQL Generation types -export type SqlGenerationRequest = { - source_config: { - type : string, - host: string, - port: number, - username: string, - password: string, - db_name: string - } - generate_sql_llm: { - model: string, - api_key?: string, - base_url?: string - }; - evaluate_sql_llm: { - model: string, - api_key?: string, - base_url?: string - }; - query: string; - result_num_limit: number; - retrieved_metadata: { - name: string; - columns: Record; - }; - evidence?: string; -}; - -export type SqlGenerationResponse = { - answer: string; - sql: string; - sql_res: { - data: any[]; - columns: string[]; - }; - input_tokens: number; - output_tokens: number; -}; - - /** - * Generate SQL and execute query using Python service + * Generate SQL and execute query with Dative Plugin + * */ export const generateAndExecuteSQL = async ({ datasetId, @@ -1300,8 +1262,8 @@ export const generateAndExecuteSQL = async ({ schema: Record; teamId: string; limit?: number; - generate_sql_llm: {model: string,api_key?: string,base_url?: string}; - evaluate_sql_llm: {model: string,api_key?: string,base_url?: string}; + generate_sql_llm: { model: string; api_key?: string; base_url?: string }; + evaluate_sql_llm: { model: string; api_key?: string; base_url?: string }; externalProvider?: { openaiAccount?: { key: string; @@ -1309,135 +1271,140 @@ export const generateAndExecuteSQL = async ({ }; }; }): Promise => { - try { - // Get dataset and database config - const dataset = await MongoDataset.findById(datasetId).lean(); - if (!dataset?.databaseConfig) { - addLog.warn('No database config found for dataset', { datasetId }); - return null; - } + // Get dataset and database config + const dataset = await MongoDataset.findById(datasetId).lean(); + if (!dataset?.databaseConfig) { + addLog.warn('No database config found for dataset', { datasetId }); + return Promise.reject(DatabaseErrEnum.dbConfigNotFound); + } - const dbConfig: any = dataset.databaseConfig; + const dbConfig: any = dataset.databaseConfig; - // Get table schema from collections - const tableNames = Object.keys(schema); - if (tableNames.length === 0) { - addLog.warn('No tables found in schema'); - return null; - } - - // Get all table schemas from MongoDB collections - const collections = await MongoDatasetCollection.find({ - datasetId, - name: { $in: tableNames }, - teamId - }).lean(); + // Get table schema from collections + const tableNames = Object.keys(schema); + if (tableNames.length === 0) { + addLog.warn('No tables found in schema'); + return null; + } - if (!collections || collections.length === 0) { - addLog.warn('No collections found for tables', { tableNames }); - return null; - } + // Get all table schemas from MongoDB collections + const collections = await MongoDatasetCollection.find({ + datasetId, + name: { $in: tableNames }, + teamId + }).lean(); - // Build table schemas for Python service - const retrievedMetadata = collections - .filter(collection => collection.tableSchema?.columns) - .map(collection => { - const columns: Record = {}; - if (collection.tableSchema?.columns) { - Object.entries(collection.tableSchema.columns).forEach((col: any) => { - columns[col.tableName] = { - name: col.colName, - type: col.type || 'varchar', - description: col.description || '' - }; - }); - } + // Collections Changes during Sql Generation + if (!collections || collections.length === 0) { + addLog.warn('No collections found for tables', { tableNames }); + return Promise.reject(`${tableNames} not found or has been deleted`); + } - return { - name: collection.name, - columns, - score: schema[collection.name]?.score || 0 + // Build table schemas for Python service + const retrievedMetadata = collections.map((collection) => { + // columns: Record + const columns: Record = {}; + if (collection.tableSchema?.columns) { + Object.values(collection.tableSchema.columns).forEach((col) => { + columns[col.columnName] = { + name: col.columnName, + type: col.columnType, + comment: col.description, + auto_increment: col.isAutoIncrement || false, + nullable: col.isNullable || false, + default: col.defaultValue || null, + examples: col.examples || [], + enabled: !col.forbid, + value_index: col.valueIndex || false }; }); - - if (retrievedMetadata.length === 0) { - addLog.warn('No valid table schemas found'); - return null; } - // Sort by score (highest first) for better SQL generation - retrievedMetadata.sort((a, b) => b.score - a.score); + const foreign_keys: DativeForeignKey[] = + collection.tableSchema?.foreignKeys?.map((fk) => ({ + name: fk.name, + column: fk.column, + referenced_schema: fk.referredSchema, + referenced_table: fk.referredTable, + referenced_column: fk.referredColumns + })) || []; + const table: DativeTable = { + name: collection.name, + ns_name: '', + comment: collection.tableSchema?.description || '', + columns, + primary_keys: collection.tableSchema?.primaryKeys || [], + foreign_keys: foreign_keys, + enable: !collection.forbid, + score: schema[collection.name]?.score || 0 + }; + console.debug('[generateAndExecuteSQL] table', table); + return table; + }); - // Get Python service URL from environment - const dativeUrl = process.env.DATIVE_BASE_URL; + if (retrievedMetadata.length === 0) { + addLog.warn('No valid table schemas found'); + return null; + } - // Get LLM config from model - const llmModelData = getLLMModel(generate_sql_llm.model); - if (!llmModelData) { - addLog.error(`Invalid LLM model specified for SQL generation ${generate_sql_llm.model}`); - return null; - } - // Update request payload to include all table schemas - const requestPayload: SqlGenerationRequest = { - source_config: { - type: dbConfig.client, - host: dbConfig.host, - port: dbConfig.port || 3306, - username: dbConfig.user, - password: dbConfig.password, - db_name: dbConfig.database - }, - generate_sql_llm, - evaluate_sql_llm, - query, - result_num_limit: limit, - retrieved_metadata: { - name: retrievedMetadata[0].name, // Primary table name - columns: retrievedMetadata.reduce((acc, table) => { - // Merge all table columns with table prefix to avoid conflicts - Object.entries(table.columns).forEach(([colName, colInfo]) => { - acc[`${table.name}.${colName}`] = { - name: colName, - type: colInfo.type, - description: `${table.name}表 - ${colInfo.description}` - }; - }); - return acc; - }, {} as Record) - } - }; + // Sort by score (highest first) for better SQL generation + retrievedMetadata.sort((a, b) => b.score - a.score); - addLog.info('Calling Python SQL generation service', { - url: `${dativeUrl}/api/v1/data_source/query_by_nl`, - tables: retrievedMetadata.map(t => t.name), - primaryTable: retrievedMetadata[0].name, - query - }); + // Get Python service URL from environment + const dativeUrl = process.env.DATIVE_BASE_URL; - // Call Python service - const response = await fetch(`${dativeUrl}/api/v1/data_source/query_by_nl`, { + // Get LLM config from model + const llmModelData = getLLMModel(generate_sql_llm.model); + if (!llmModelData) { + addLog.error(`Invalid LLM model specified for SQL generation ${generate_sql_llm.model}`); + return Promise.reject( + `Invalid LLM model specified for SQL generation ${generate_sql_llm.model}` + ); + } + // Update request payload to include all table schemas + const requestPayload: SqlGenerationRequest = { + source_config: { + type: dbConfig.client, + host: dbConfig.host, + port: dbConfig.port || 3306, + username: dbConfig.user, + password: dbConfig.password, + db_name: dbConfig.database + }, + generate_sql_llm, + evaluate_sql_llm, + query, + result_num_limit: limit, + retrieved_metadata: { + name: dbConfig.database, // DatabaseName + comments: '', + tables: retrievedMetadata + } + }; + let response: Response; + + try { + response = await fetch(`${dativeUrl}/api/v1/data_source/query_by_nl`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(requestPayload) }); - - if (!response.ok) { - const errorText = await response.text(); - addLog.error('Python SQL service error', { - status: response.status, - statusText: response.statusText, - error: errorText - }); - return null; - } - - const result: SqlGenerationResponse = await response.json(); - return result; - - } catch (error) { - addLog.error('SQL generation failed', error); - return null; + } catch (error: any) { + addLog.error('Error connecting to Dative service', error); + return Promise.reject(DatabaseErrEnum.dativeServiceError); } -}; \ No newline at end of file + if (!response.ok) { + const errorText = await response.text(); + addLog.error('[generateAndExecuteSQL]:', { + status: response.status, + statusText: response.statusText, + error: errorText + }); + return Promise.reject(i18nT('chat:language_model_error')); + } + + const result: SqlGenerationResponse = await response.json(); + return result; +}; diff --git a/packages/service/core/evaluation/common.ts b/packages/service/core/evaluation/common.ts index becc4c9b57e2..c1993c8ab886 100644 --- a/packages/service/core/evaluation/common.ts +++ b/packages/service/core/evaluation/common.ts @@ -451,12 +451,14 @@ export const authEvaluationDatasetDataUpdateById = async ( tmbId: string; collectionId: string; }> => { - const dataItem = await MongoEvalDatasetData.findById(dataId).select('datasetId').lean(); + const dataItem = await MongoEvalDatasetData.findById(dataId) + .select('evalDatasetCollectionId') + .lean(); if (!dataItem) { throw new Error(EvaluationErrEnum.datasetDataNotFound); } - const collectionId = String(dataItem.datasetId); + const collectionId = String(dataItem.evalDatasetCollectionId); return await authEvaluationDatasetDataUpdate(collectionId, auth); }; @@ -468,11 +470,13 @@ export const authEvaluationDatasetDataReadById = async ( tmbId: string; collectionId: string; }> => { - const dataItem = await MongoEvalDatasetData.findById(dataId).select('datasetId').lean(); + const dataItem = await MongoEvalDatasetData.findById(dataId) + .select('evalDatasetCollectionId') + .lean(); if (!dataItem) { throw new Error(EvaluationErrEnum.datasetDataNotFound); } - const collectionId = String(dataItem.datasetId); + const collectionId = String(dataItem.evalDatasetCollectionId); return await authEvaluationDatasetDataRead(collectionId, auth); }; diff --git a/packages/service/core/evaluation/dataset/csvUtils.ts b/packages/service/core/evaluation/dataset/csvUtils.ts new file mode 100644 index 000000000000..0bd75179f806 --- /dev/null +++ b/packages/service/core/evaluation/dataset/csvUtils.ts @@ -0,0 +1,116 @@ +import Papa from 'papaparse'; + +// Interface for CSV row data structure +export interface CSVRow { + user_input: string; + expected_output: string; + actual_output?: string; + context?: string; + retrieval_context?: string; + metadata?: string; +} + +// Required CSV columns that must be present +export const REQUIRED_CSV_COLUMNS = ['user_input', 'expected_output'] as const; + +// Optional CSV columns that can be present +export const OPTIONAL_CSV_COLUMNS = [ + 'actual_output', + 'context', + 'retrieval_context', + 'metadata' +] as const; + +// All valid CSV columns +export const CSV_COLUMNS = [...REQUIRED_CSV_COLUMNS, ...OPTIONAL_CSV_COLUMNS] as const; + +// Enum to CSV mapping for header normalization +export const ENUM_TO_CSV_MAPPING = { + userInput: 'user_input', + expectedOutput: 'expected_output', + actualOutput: 'actual_output', + context: 'context', + retrievalContext: 'retrieval_context' +} as const; + +/** + * Normalize header names by mapping enum values to CSV column names + * @param header - The header string to normalize + * @returns The normalized header name + */ +function normalizeHeaderName(header: string): string { + // For most CSV files, headers should be used as-is + // Only apply mapping if the header matches an enum value exactly + const mappedValue = ENUM_TO_CSV_MAPPING[header as keyof typeof ENUM_TO_CSV_MAPPING]; + return mappedValue || header; +} + +/** + * Parse CSV content using Papa Parse with optimized performance settings + * @param csvContent - The raw CSV content string + * @returns Array of parsed CSV rows + * @throws Error if CSV parsing fails or required columns are missing + */ +export function parseCSVContent(csvContent: string): CSVRow[] { + if (!csvContent.trim()) { + return []; + } + + // Parse CSV with Papa Parse for optimal performance + const parseResult = Papa.parse(csvContent, { + header: true, + skipEmptyLines: true, + fastMode: false, // Disable fastMode to handle complex quoted fields + transformHeader: (header: string) => { + // Remove quotes and normalize header names + const cleanHeader = header.replace(/^"|"$/g, '').trim(); + return normalizeHeaderName(cleanHeader); + } + }); + + if (parseResult.errors.length > 0) { + const error = parseResult.errors[0]; + throw new Error(`CSV parsing error at row ${error.row + 1}: ${error.message}`); + } + + const data = parseResult.data as Record[]; + + if (data.length === 0) { + return []; + } + + // Get normalized headers from the first data row + const headers = Object.keys(data[0]); + + // Validate CSV structure + const missingColumns = REQUIRED_CSV_COLUMNS.filter((col) => !headers.includes(col)); + if (missingColumns.length > 0) { + throw new Error(`CSV file is missing required columns: ${missingColumns.join(', ')}`); + } + + // Convert to CSVRow format + const rows: CSVRow[] = data.map((rowData) => { + const row: CSVRow = { + user_input: (rowData.user_input || '').trim(), + expected_output: (rowData.expected_output || '').trim() + }; + + // Add optional fields if they exist + if ('actual_output' in rowData) { + row.actual_output = (rowData.actual_output || '').trim(); + } + if ('context' in rowData) { + row.context = (rowData.context || '').trim(); + } + if ('retrieval_context' in rowData) { + row.retrieval_context = (rowData.retrieval_context || '').trim(); + } + if ('metadata' in rowData) { + row.metadata = (rowData.metadata || '{}').trim(); + } + + return row; + }); + + return rows; +} diff --git a/packages/service/core/evaluation/dataset/dataQualityMq.ts b/packages/service/core/evaluation/dataset/dataQualityMq.ts index d592b2379935..ce4d1ed9ddcf 100644 --- a/packages/service/core/evaluation/dataset/dataQualityMq.ts +++ b/packages/service/core/evaluation/dataset/dataQualityMq.ts @@ -1,5 +1,5 @@ import { getQueue, getWorker, QueueNames } from '../../../common/bullmq'; -import { type Processor } from 'bullmq'; +import { type Processor, type Queue } from 'bullmq'; import { addLog } from '../../../common/system/log'; import { createJobCleaner, @@ -20,22 +20,19 @@ export const evalDatasetDataQualityQueue = getQueue( backoff: { type: 'exponential', delay: 1000 - } + }, + removeOnFail: false } } ); -const concurrency = process.env.EVAL_DATA_QUALITY_CONCURRENCY - ? Number(process.env.EVAL_DATA_QUALITY_CONCURRENCY) - : 2; +const concurrency = global.systemEnv?.evalConfig?.dataQualityConcurrency || 2; export const getEvalDatasetDataQualityWorker = ( processor: Processor ) => { return getWorker(QueueNames.evalDatasetDataQuality, processor, { - removeOnFail: { - count: 1000 // Keep last 1000 failed jobs - }, + maxStalledCount: global.systemEnv?.evalConfig?.maxStalledCount || 3, concurrency: concurrency }); }; @@ -62,6 +59,25 @@ export const checkEvalDatasetDataQualityJobActive = async (dataId: string): Prom } }; +export const checkEvalDatasetDataQualityJobInactive = async (dataId: string): Promise => { + try { + const jobId = await evalDatasetDataQualityQueue.getDeduplicationJobId(String(dataId)); + if (!jobId) return false; + + const job = await evalDatasetDataQualityQueue.getJob(jobId); + if (!job) return false; + + const jobState = await job.getState(); + return ['completed', 'failed'].includes(jobState); + } catch (error) { + addLog.error('Failed to check eval dataset data quality job inactive status', { + dataId, + error + }); + return false; + } +}; + export const removeEvalDatasetDataQualityJobsRobust = async ( dataIds: string[], options?: JobCleanupOptions @@ -85,3 +101,19 @@ export const removeEvalDatasetDataQualityJobsRobust = async ( return result; }; + +export const checkBullMQHealth = async (queue: Queue, queueName: string): Promise => { + try { + await queue.isPaused(); + await queue.getWaiting(0, 0); + } catch (error) { + addLog.error(`BullMQ ${queueName} queue health check failed:`, error); + throw new Error( + `BullMQ ${queueName} queue is not responding. Please check Redis connection and queue status.` + ); + } +}; + +export const checkEvalDatasetDataQualityQueueHealth = (): Promise => { + return checkBullMQHealth(evalDatasetDataQualityQueue, 'quality'); +}; diff --git a/packages/service/core/evaluation/dataset/dataQualityProcessor.ts b/packages/service/core/evaluation/dataset/dataQualityProcessor.ts index 3f880a6e21dd..1004aefb9162 100644 --- a/packages/service/core/evaluation/dataset/dataQualityProcessor.ts +++ b/packages/service/core/evaluation/dataset/dataQualityProcessor.ts @@ -4,7 +4,8 @@ import { MongoEvalDatasetData } from './evalDatasetDataSchema'; import { getEvalDatasetDataQualityWorker, type EvalDatasetDataQualityData } from './dataQualityMq'; import { EvalDatasetDataKeyEnum, - EvalDatasetDataQualityStatusEnum + EvalDatasetDataQualityStatusEnum, + EvalDatasetDataQualityResultEnum } from '@fastgpt/global/core/evaluation/dataset/constants'; import { EvalMetricTypeEnum } from '@fastgpt/global/core/evaluation/metric/constants'; import type { EvalMetricSchemaType } from '@fastgpt/global/core/evaluation/metric/type'; @@ -27,9 +28,9 @@ export const processEvalDatasetDataQuality = async (job: Job= 0.7 - ? EvalDatasetDataQualityStatusEnum.highQuality - : EvalDatasetDataQualityStatusEnum.needsOptimization; + ? EvalDatasetDataQualityResultEnum.highQuality + : EvalDatasetDataQualityResultEnum.needsOptimization; await MongoEvalDatasetData.findByIdAndUpdate(dataId, { $set: { - 'metadata.qualityStatus': qualityStatus, - 'metadata.qualityScore': metricResult.data.score, - 'metadata.qualityReason': metricResult.data?.reason, - 'metadata.qualityRunLogs': metricResult.data?.runLogs, - 'metadata.qualityUsages': metricResult?.usages, - 'metadata.qualityFinishTime': new Date(), - 'metadata.qualityModel': evaluationModel + 'qualityMetadata.status': EvalDatasetDataQualityStatusEnum.completed, + 'qualityMetadata.score': metricResult.data.score, + 'qualityMetadata.reason': metricResult.data?.reason, + 'qualityMetadata.runLogs': metricResult.data?.runLogs, + 'qualityMetadata.usages': metricResult?.usages, + 'qualityMetadata.finishTime': new Date(), + 'qualityMetadata.model': evaluationModel, + qualityResult: qualityResult } }); @@ -131,9 +133,9 @@ export const processEvalDatasetDataQuality = async (job: Job ) => { return getWorker(QueueNames.evalDatasetDataSynthesize, processor, { - removeOnFail: { - count: 1000 // Keep last 1000 failed jobs for debugging - }, + maxStalledCount: global.systemEnv?.evalConfig?.maxStalledCount || 3, concurrency: concurrency }); }; @@ -87,3 +85,7 @@ export const removeEvalDatasetDataSynthesizeJobsRobust = async ( return result; }; + +export const checkEvalDatasetDataSynthesizeQueueHealth = (): Promise => { + return checkBullMQHealth(evalDatasetDataSynthesizeQueue, 'synthesis'); +}; diff --git a/packages/service/core/evaluation/dataset/dataSynthesizeProcessor.ts b/packages/service/core/evaluation/dataset/dataSynthesizeProcessor.ts index 722e2db8bff9..cb7bba31318e 100644 --- a/packages/service/core/evaluation/dataset/dataSynthesizeProcessor.ts +++ b/packages/service/core/evaluation/dataset/dataSynthesizeProcessor.ts @@ -6,7 +6,8 @@ import { MongoDatasetData } from '../../dataset/data/schema'; import { EvalDatasetDataCreateFromEnum, EvalDatasetDataKeyEnum, - EvalDatasetDataQualityStatusEnum + EvalDatasetDataQualityStatusEnum, + EvalDatasetDataQualityResultEnum } from '@fastgpt/global/core/evaluation/dataset/constants'; import type { EvalDatasetDataSchemaType } from '@fastgpt/global/core/evaluation/dataset/type'; import { @@ -66,30 +67,36 @@ async function processor(job: Job) { totalPoints = calculatedPoints; } + const qualityResult = + synthesisResult.data?.metadata?.score && synthesisResult.data.metadata.score >= 0.7 + ? EvalDatasetDataQualityResultEnum.highQuality + : EvalDatasetDataQualityResultEnum.needsOptimization; + const evalData: Partial = { teamId: evalDatasetCollection.teamId, tmbId: evalDatasetCollection.tmbId, - datasetId: evalDatasetCollectionId, + evalDatasetCollectionId: evalDatasetCollectionId, [EvalDatasetDataKeyEnum.UserInput]: synthesisResult.data?.qaPair.question, [EvalDatasetDataKeyEnum.ExpectedOutput]: synthesisResult.data?.qaPair.answer, [EvalDatasetDataKeyEnum.ActualOutput]: '', [EvalDatasetDataKeyEnum.Context]: [], [EvalDatasetDataKeyEnum.RetrievalContext]: [], - metadata: { - sourceDataId: sourceData._id, - sourceDatasetId: sourceData.datasetId, - sourceCollectionId: sourceData.collectionId, - qualityScore: synthesisResult.data?.metadata?.score, - qualityReason: synthesisResult.data?.metadata?.reason, - qualityUsages: synthesisResult?.usages, - qualityStatus: - synthesisResult.data?.metadata?.score && synthesisResult.data.metadata.score >= 0.7 - ? EvalDatasetDataQualityStatusEnum.highQuality - : EvalDatasetDataQualityStatusEnum.needsOptimization, + qualityMetadata: { + status: EvalDatasetDataQualityStatusEnum.completed, + score: synthesisResult.data?.metadata?.score, + reason: synthesisResult.data?.metadata?.reason, + usages: synthesisResult?.usages, + finishTime: new Date() + }, + synthesisMetadata: { + sourceDataId: sourceData._id.toString(), + sourceDatasetId: sourceData.datasetId.toString(), + sourceCollectionId: sourceData.collectionId.toString(), + intelligentGenerationModel, generatedAt: new Date(), - synthesizedAt: new Date(), - intelligentGenerationModel + synthesizedAt: new Date() }, + qualityResult, createFrom: EvalDatasetDataCreateFromEnum.intelligentGeneration }; diff --git a/packages/service/core/evaluation/dataset/evalDatasetDataSchema.ts b/packages/service/core/evaluation/dataset/evalDatasetDataSchema.ts index da6c81de04bb..4baa8e213cbe 100644 --- a/packages/service/core/evaluation/dataset/evalDatasetDataSchema.ts +++ b/packages/service/core/evaluation/dataset/evalDatasetDataSchema.ts @@ -4,7 +4,10 @@ import { EvalDatasetCollectionName } from './evalDatasetCollectionSchema'; import { EvalDatasetDataCreateFromEnum, EvalDatasetDataCreateFromValues, - EvalDatasetDataKeyEnum + EvalDatasetDataKeyEnum, + EvalDatasetDataQualityStatusEnum, + EvalDatasetDataQualityResultEnum, + EvalDatasetDataQualityResultValues } from '@fastgpt/global/core/evaluation/dataset/constants'; import { TeamCollectionName, @@ -27,7 +30,7 @@ const EvalDatasetDataSchema = new Schema({ ref: TeamMemberCollectionName, required: true }, - datasetId: { + evalDatasetCollectionId: { type: Schema.Types.ObjectId, ref: EvalDatasetCollectionName, required: true, @@ -70,9 +73,38 @@ const EvalDatasetDataSchema = new Schema({ ], default: [] }, - metadata: { - type: Schema.Types.Mixed, - default: {} + qualityMetadata: { + status: { + type: String, + enum: Object.values(EvalDatasetDataQualityStatusEnum), + default: EvalDatasetDataQualityStatusEnum.unevaluated, + required: true + }, + score: { + type: Number, + min: 0, + max: 1 + }, + reason: String, + model: String, + usages: [Schema.Types.Mixed], + runLogs: [Schema.Types.Mixed], + startTime: Date, + finishTime: Date, + queueTime: Date, + error: String + }, + synthesisMetadata: { + sourceDataId: String, + sourceDatasetId: String, + sourceCollectionId: String, + intelligentGenerationModel: String, + synthesizedAt: Date, + generatedAt: Date + }, + qualityResult: { + type: String, + enum: EvalDatasetDataQualityResultValues }, createFrom: { type: String, @@ -93,8 +125,13 @@ const EvalDatasetDataSchema = new Schema({ }); // Indexes for efficient queries -EvalDatasetDataSchema.index({ datasetId: 1, createTime: -1 }); -EvalDatasetDataSchema.index({ datasetId: 1, updateTime: -1 }); +EvalDatasetDataSchema.index({ evalDatasetCollectionId: 1, createTime: -1 }); +EvalDatasetDataSchema.index({ evalDatasetCollectionId: 1, updateTime: -1 }); + +// Quality related indexes +EvalDatasetDataSchema.index({ 'qualityMetadata.status': 1 }); +EvalDatasetDataSchema.index({ qualityResult: 1 }); +EvalDatasetDataSchema.index({ evalDatasetCollectionId: 1, qualityResult: 1 }); // Text search index for searching within inputs and outputs EvalDatasetDataSchema.index({ diff --git a/packages/service/core/evaluation/dataset/smartGenerateMq.ts b/packages/service/core/evaluation/dataset/smartGenerateMq.ts deleted file mode 100644 index 9eacfc789f86..000000000000 --- a/packages/service/core/evaluation/dataset/smartGenerateMq.ts +++ /dev/null @@ -1,91 +0,0 @@ -import { getQueue, getWorker, QueueNames } from '../../../common/bullmq'; -import { type Processor } from 'bullmq'; -import { addLog } from '../../../common/system/log'; -import { - createJobCleaner, - type JobCleanupResult, - type JobCleanupOptions -} from '../utils/jobCleanup'; - -export type EvalDatasetSmartGenerateData = { - datasetCollectionIds: string[]; - count?: number; - keywords?: string[]; - intelligentGenerationModel: string; - evalDatasetCollectionId: string; -}; - -export const evalDatasetSmartGenerateQueue = getQueue( - QueueNames.evalDatasetSmartGenerate, - { - defaultJobOptions: { - attempts: 3, - backoff: { - type: 'exponential', - delay: 2000 - } - } - } -); - -const concurrency = process.env.EVAL_DATASET_SMART_GENERATE_CONCURRENCY - ? Number(process.env.EVAL_DATASET_SMART_GENERATE_CONCURRENCY) - : 2; - -export const getEvalDatasetSmartGenerateWorker = ( - processor: Processor -) => { - return getWorker(QueueNames.evalDatasetSmartGenerate, processor, { - removeOnFail: { - count: 1000 // Keep last 1000 failed jobs for debugging - }, - concurrency: concurrency - }); -}; - -export const addEvalDatasetSmartGenerateJob = (data: EvalDatasetSmartGenerateData) => { - const jobId = `smartgen-${data.evalDatasetCollectionId}-${Date.now()}`; - - return evalDatasetSmartGenerateQueue.add(jobId, data, { - deduplication: { id: jobId } - }); -}; - -export const checkEvalDatasetSmartGenerateJobActive = async ( - evalDatasetCollectionId: string -): Promise => { - try { - const jobs = await evalDatasetSmartGenerateQueue.getJobs(['waiting', 'active', 'delayed']); - return jobs.some((job) => job.data.evalDatasetCollectionId === evalDatasetCollectionId); - } catch (error) { - addLog.error('Failed to check eval dataset smart generate job status', { - evalDatasetCollectionId: evalDatasetCollectionId, - error - }); - return false; - } -}; - -export const removeEvalDatasetSmartGenerateJobsRobust = async ( - evalDatasetCollectionIds: string[], - options?: JobCleanupOptions -): Promise => { - const cleaner = createJobCleaner(options); - - const filterFn = (job: any) => { - return evalDatasetCollectionIds.includes(String(job.data?.evalDatasetCollectionId)); - }; - - const result = await cleaner.cleanAllJobsByFilter( - evalDatasetSmartGenerateQueue, - filterFn, - QueueNames.evalDatasetSmartGenerate - ); - - addLog.info('Evaluation DatasetData Smart generate jobs cleanup completed', { - evalDatasetCollectionIds: evalDatasetCollectionIds.length, - result - }); - - return result; -}; diff --git a/packages/service/core/evaluation/dataset/smartGenerateProcessor.ts b/packages/service/core/evaluation/dataset/smartGenerateProcessor.ts deleted file mode 100644 index f874deafd286..000000000000 --- a/packages/service/core/evaluation/dataset/smartGenerateProcessor.ts +++ /dev/null @@ -1,198 +0,0 @@ -import type { Job } from 'bullmq'; -import type { HydratedDocument } from 'mongoose'; -import { Types } from '../../../common/mongo'; -import { readFromSecondary } from '../../../common/mongo/utils'; -import { addLog } from '../../../common/system/log'; -import { MongoEvalDatasetCollection } from './evalDatasetCollectionSchema'; -import { MongoEvalDatasetData } from './evalDatasetDataSchema'; -import { MongoDatasetData } from '../../dataset/data/schema'; -import { - EvalDatasetDataCreateFromEnum, - EvalDatasetDataKeyEnum, - EvalDatasetDataQualityStatusEnum -} from '@fastgpt/global/core/evaluation/dataset/constants'; -import type { EvalDatasetDataSchemaType } from '@fastgpt/global/core/evaluation/dataset/type'; -import { - type EvalDatasetSmartGenerateData, - getEvalDatasetSmartGenerateWorker -} from './smartGenerateMq'; -import { addEvalDatasetDataSynthesizeJob } from './dataSynthesizeMq'; -import { checkTeamAIPoints } from '../../../support/permission/teamLimit'; -import { addAuditLog } from '../../../support/user/audit/util'; -import { AuditEventEnum } from '@fastgpt/global/support/user/audit/constants'; - -async function processor(job: Job) { - const { datasetCollectionIds, count, intelligentGenerationModel, evalDatasetCollectionId } = - job.data; - - if (!global.llmModelMap.has(intelligentGenerationModel)) { - const errorMsg = `Invalid intelligent generation model: ${intelligentGenerationModel}`; - addLog.error('Eval dataset smart generation failed - invalid model', { - evalDatasetCollectionId, - intelligentGenerationModel - }); - throw new Error(errorMsg); - } - - try { - addLog.info('Starting eval dataset smart generation', { - evalDatasetCollectionId, - datasetCollectionIds, - count, - intelligentGenerationModel - }); - - const sampleSize = Number(count); - if (!Number.isInteger(sampleSize) || sampleSize <= 0) { - throw new Error(`Invalid count parameter: ${count}. Must be a positive integer.`); - } - - const evalDatasetCollection = - await MongoEvalDatasetCollection.findById(evalDatasetCollectionId); - if (!evalDatasetCollection) { - throw new Error(`Eval dataset collection not found: ${evalDatasetCollectionId}`); - } - - await checkTeamAIPoints(evalDatasetCollection.teamId); - - const match = { - teamId: new Types.ObjectId(evalDatasetCollection.teamId), - collectionId: { $in: datasetCollectionIds.map((id) => new Types.ObjectId(id)) } - }; - - const sampleData = await MongoDatasetData.aggregate( - [ - { - $match: match - }, - { - $sample: { size: sampleSize } - }, - { - $project: { - q: 1, - a: 1, - datasetId: 1, - collectionId: 1 - } - } - ], - { - ...readFromSecondary - } - ); - - if (sampleData.length === 0) { - throw new Error('No data found in selected dataset collections'); - } - - addLog.info('Retrieved sample data for generation', { - evalDatasetCollectionId: evalDatasetCollectionId, - sampleCount: sampleData.length - }); - - const generateData: Array> = []; - const synthesisData: Array<{ - dataId: string; - intelligentGenerationModel: string; - evalDatasetCollectionId: string; - }> = []; - - for (const sample of sampleData) { - if (sample.q && sample.a) { - // Direct QA pair - can be used immediately - const evalData: Partial = { - teamId: evalDatasetCollection.teamId, - tmbId: evalDatasetCollection.tmbId, - datasetId: evalDatasetCollectionId, - [EvalDatasetDataKeyEnum.UserInput]: sample.q, - [EvalDatasetDataKeyEnum.ExpectedOutput]: sample.a, - [EvalDatasetDataKeyEnum.ActualOutput]: '', - [EvalDatasetDataKeyEnum.Context]: [], - [EvalDatasetDataKeyEnum.RetrievalContext]: [], - metadata: { - sourceDataId: sample._id, - sourceDatasetId: sample.datasetId, - sourceCollectionId: sample.collectionId, - qualityStatus: EvalDatasetDataQualityStatusEnum.unevaluated, - generatedAt: new Date(), - intelligentGenerationModel - }, - createFrom: EvalDatasetDataCreateFromEnum.intelligentGeneration - }; - generateData.push(evalData); - } else if (sample.q && sample.a === '') { - // Only Q - add to synthesis data list (not saved to mongo here) - synthesisData.push({ - dataId: sample._id.toString(), - intelligentGenerationModel, - evalDatasetCollectionId - }); - } - } - - // Bulk insert complete evaluation dataset data - let insertedRecords: HydratedDocument[] = []; - if (generateData.length > 0) { - insertedRecords = await MongoEvalDatasetData.insertMany(generateData, { - ordered: false - }); - - addLog.info('Inserted complete eval dataset data', { - evalDatasetCollectionId: evalDatasetCollectionId, - insertedCount: insertedRecords.length - }); - } - - // Queue synthesis jobs for data that needs processing (synthesisData) - const synthesizeJobs = []; - for (const synthData of synthesisData) { - const synthesizeJob = await addEvalDatasetDataSynthesizeJob(synthData); - synthesizeJobs.push(synthesizeJob); - } - - if (synthesizeJobs.length > 0) { - addLog.info('Queued synthesis jobs', { - evalDatasetCollectionId: evalDatasetCollectionId, - synthesizeJobsCount: synthesizeJobs.length - }); - } - - (async () => { - addAuditLog({ - teamId: evalDatasetCollection.teamId, - tmbId: evalDatasetCollection.tmbId, - event: AuditEventEnum.SMART_GENERATE_EVALUATION_DATA, - params: { - collectionName: evalDatasetCollection.name - } - }); - })(); - - addLog.info('Completed eval dataset smart generation', { - evalDatasetCollectionId: evalDatasetCollectionId, - generateDataCount: insertedRecords.length, - synthesisDataCount: synthesisData.length, - synthesizeJobsCount: synthesizeJobs.length - }); - - return { - success: true, - generateDataCount: insertedRecords.length, - synthesisDataCount: synthesisData.length, - synthesizeJobsCount: synthesizeJobs.length - }; - } catch (error) { - addLog.error('Failed to process eval dataset smart generation', { - evalDatasetCollectionId: evalDatasetCollectionId, - error: error instanceof Error ? error.message : String(error), - stack: error instanceof Error ? error.stack : undefined - }); - throw error; - } -} - -// Initialize worker -export const initEvalDatasetSmartGenerateWorker = () => { - return getEvalDatasetSmartGenerateWorker(processor); -}; diff --git a/packages/service/core/evaluation/dataset/utils.ts b/packages/service/core/evaluation/dataset/utils.ts index 2c3a69571205..ec39ef10883c 100644 --- a/packages/service/core/evaluation/dataset/utils.ts +++ b/packages/service/core/evaluation/dataset/utils.ts @@ -1,6 +1,10 @@ import type { EvalDatasetCollectionStatus } from '@fastgpt/global/core/evaluation/dataset/type'; import { EvalDatasetCollectionStatusEnum } from '@fastgpt/global/core/evaluation/dataset/constants'; import { evalDatasetDataSynthesizeQueue } from './dataSynthesizeMq'; +import { getEvaluationPermissionAggregation } from '../common'; +import { replaceRegChars } from '@fastgpt/global/common/string/tools'; +import { Types } from 'mongoose'; +import type { ApiRequestProps } from '../../../type/next'; export async function getCollectionStatus( collectionId: string @@ -10,7 +14,8 @@ export async function getCollectionStatus( 'waiting', 'active', 'delayed', - 'failed' + 'failed', + 'completed' ]); const collectionJobs = jobs.filter((job) => job.data.evalDatasetCollectionId === collectionId); @@ -18,22 +23,36 @@ export async function getCollectionStatus( return EvalDatasetCollectionStatusEnum.ready; } - if (collectionJobs.some((job) => job.isFailed())) { - return EvalDatasetCollectionStatusEnum.error; - } + let hasActive = false; + let hasWaiting = false; + let hasFailed = false; - if (collectionJobs.some((job) => job.isActive())) { - return EvalDatasetCollectionStatusEnum.processing; + for (const job of collectionJobs) { + if (await job.isActive()) { + hasActive = true; + } else if ((await job.isWaiting()) || (await job.isDelayed())) { + hasWaiting = true; + } else if (await job.isFailed()) { + hasFailed = true; + } } - if (collectionJobs.some((job) => job.isWaiting() || job.isDelayed())) { + if (hasWaiting) { return EvalDatasetCollectionStatusEnum.queuing; } + if (hasActive) { + return EvalDatasetCollectionStatusEnum.processing; + } + if (hasFailed) { + return EvalDatasetCollectionStatusEnum.error; + } return EvalDatasetCollectionStatusEnum.ready; } catch (error) { console.error('Error getting collection status:', error); - return EvalDatasetCollectionStatusEnum.ready; + throw new Error( + `Failed to get collection status: ${error instanceof Error ? error.message : String(error)}` + ); } } @@ -51,7 +70,7 @@ export function buildCollectionAggregationPipeline(baseFields?: Record, + searchKey?: string +) { + // 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) + }; + + if (searchKey && typeof searchKey === 'string' && searchKey.trim().length > 0) { + baseFilter.name = { $regex: new RegExp(`${replaceRegChars(searchKey.trim())}`, 'i') }; + } + + // 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) + }; + } + } + + return { + finalFilter, + teamId, + tmbId, + isOwner, + myRoles, + accessibleIds, + roleList + }; +} diff --git a/packages/service/core/evaluation/evaluator/index.ts b/packages/service/core/evaluation/evaluator/index.ts index a3b008f0fb6e..c2139e5c1b18 100644 --- a/packages/service/core/evaluation/evaluator/index.ts +++ b/packages/service/core/evaluation/evaluator/index.ts @@ -166,7 +166,7 @@ export class DitingEvaluator extends Evaluator { llmConfig?: EvalModelConfigType, embeddingConfig?: EvalModelConfigType, evaluatorConfig?: EvaluatorSchema, - scoreScaling: number = 100 + scoreScaling: number = 1 ) { super(metricConfig, llmConfig, embeddingConfig, evaluatorConfig); this.client = createDitingClient(); @@ -207,7 +207,7 @@ export class DitingEvaluator extends Evaluator { } // Apply score scaling if data.score exists - // scoreScaling directly multiplies the original score (e.g., 100 means 100x amplification) + // scoreScaling directly multiplies the original score (e.g., 1 means no scaling, 100 means 100x amplification) let scaledData = response.data; if (response.data?.score !== undefined && response.data?.score !== null) { scaledData = { @@ -266,7 +266,7 @@ export async function createEvaluatorInstance( } } - const scoreScaling = evaluatorConfig.scoreScaling ?? 100; + const scoreScaling = evaluatorConfig.scoreScaling ?? 1; const evaluatorInstance = new DitingEvaluator( metricConfig, llmConfig, diff --git a/packages/service/core/evaluation/index.ts b/packages/service/core/evaluation/index.ts index e5ecc41f0089..3e9cc2eeaa08 100644 --- a/packages/service/core/evaluation/index.ts +++ b/packages/service/core/evaluation/index.ts @@ -1,8 +1,19 @@ import { addLog } from '../../common/system/log'; import { initEvalDatasetDataQualityWorker } from './dataset/dataQualityProcessor'; import { initEvalDatasetDataSynthesizeWorker } from './dataset/dataSynthesizeProcessor'; -import { initEvalDatasetSmartGenerateWorker } from './dataset/smartGenerateProcessor'; import { initEvalTaskWorker, initEvalTaskItemWorker } from './task/processor'; +import { initEvaluationSummaryWorker } from './summary/worker'; + +// Import all queues for cleanup +import { evaluationTaskQueue, evaluationItemQueue } from './task/mq'; +import { evalDatasetDataQualityQueue } from './dataset/dataQualityMq'; +import { evalDatasetDataSynthesizeQueue } from './dataset/dataSynthesizeMq'; +import { getEvaluationSummaryQueue } from './summary/queue'; + +// Import MongoDB models for existence checks +import { MongoEvaluation, MongoEvalItem } from './task/schema'; +import { MongoEvalDatasetData } from './dataset/evalDatasetDataSchema'; +import { MongoEvalDatasetCollection } from './dataset/evalDatasetCollectionSchema'; // Initialize evaluation workers @@ -13,6 +24,215 @@ export const initEvaluationWorkers = () => { initEvalTaskItemWorker(); initEvalDatasetDataQualityWorker(); - initEvalDatasetSmartGenerateWorker(); initEvalDatasetDataSynthesizeWorker(); + + initEvaluationSummaryWorker(); + + // Setup periodic orphaned jobs cleanup + setupOrphanedJobsCleanup(); +}; + +/** + * Setup periodic cleanup for orphaned jobs + * Specifically handles residual issues caused by active jobs that cannot be deleted + */ +const setupOrphanedJobsCleanup = () => { + // Initial delay of 5 minutes before starting cleanup (let system stabilize) + setTimeout( + async () => { + addLog.info('[Evaluation] Running initial orphaned jobs cleanup...'); + await cleanupOrphanedJobs(); + }, + 5 * 60 * 1000 + ); + + // Then run cleanup every 30 minutes + setInterval( + async () => { + await cleanupOrphanedJobs(); + }, + 30 * 60 * 1000 + ); + + addLog.info('[Evaluation] Orphaned jobs cleanup scheduled (every 30 minutes)'); }; + +/** + * Comprehensive cleanup for all orphaned jobs in evaluation system + * Handles active jobs that cannot be deleted by BullMQ + */ +export const cleanupOrphanedJobs = async () => { + try { + addLog.debug('[Evaluation] Starting comprehensive orphaned jobs cleanup'); + + const summaryQueue = getEvaluationSummaryQueue(); + + // Get all jobs from all evaluation queues + const [taskJobs, itemJobs, dataQualityJobs, dataSynthesizeJobs, summaryJobs] = + await Promise.all([ + evaluationTaskQueue.getJobs( + ['active', 'waiting', 'delayed', 'completed', 'failed'], + 0, + 1000 + ), + evaluationItemQueue.getJobs( + ['active', 'waiting', 'delayed', 'completed', 'failed'], + 0, + 2000 + ), + evalDatasetDataQualityQueue.getJobs( + ['active', 'waiting', 'delayed', 'completed', 'failed'], + 0, + 1000 + ), + evalDatasetDataSynthesizeQueue.getJobs( + ['active', 'waiting', 'delayed', 'completed', 'failed'], + 0, + 1000 + ), + summaryQueue.getJobs(['active', 'waiting', 'delayed', 'completed', 'failed'], 0, 500) + ]); + + let cleanedCount = 0; + let skippedActiveCount = 0; + + // 1. Clean orphaned task jobs + for (const job of taskJobs) { + try { + const { evalId } = job.data; + const evaluation = await MongoEvaluation.exists({ _id: evalId }); + + if (!evaluation) { + const result = await cleanupJob(job, 'task', { evalId }); + if (result.cleaned) cleanedCount++; + if (result.skippedActive) skippedActiveCount++; + } + } catch (error) { + addLog.warn('[Evaluation] Failed to cleanup task job', { jobId: job.id, error }); + } + } + + // 2. Clean orphaned item jobs + for (const job of itemJobs) { + try { + const { evalId, evalItemId } = job.data; + const [evaluation, evalItem] = await Promise.all([ + MongoEvaluation.exists({ _id: evalId }), + MongoEvalItem.exists({ _id: evalItemId }) + ]); + + if (!evaluation || !evalItem) { + const result = await cleanupJob(job, 'item', { evalId, evalItemId }); + if (result.cleaned) cleanedCount++; + if (result.skippedActive) skippedActiveCount++; + } + } catch (error) { + addLog.warn('[Evaluation] Failed to cleanup item job', { jobId: job.id, error }); + } + } + + // 3. Clean orphaned data quality jobs + for (const job of dataQualityJobs) { + try { + const { dataId } = job.data; + const dataExists = await MongoEvalDatasetData.exists({ _id: dataId }); + + if (!dataExists) { + const result = await cleanupJob(job, 'dataQuality', { dataId }); + if (result.cleaned) cleanedCount++; + if (result.skippedActive) skippedActiveCount++; + } + } catch (error) { + addLog.warn('[Evaluation] Failed to cleanup data quality job', { jobId: job.id, error }); + } + } + + // 4. Clean orphaned data synthesize jobs + for (const job of dataSynthesizeJobs) { + try { + const { dataId, evalDatasetCollectionId } = job.data; + const [dataExists, collectionExists] = await Promise.all([ + MongoEvalDatasetData.exists({ _id: dataId }), + MongoEvalDatasetCollection.exists({ _id: evalDatasetCollectionId }) + ]); + + if (!dataExists || !collectionExists) { + const result = await cleanupJob(job, 'dataSynthesize', { + dataId, + evalDatasetCollectionId + }); + if (result.cleaned) cleanedCount++; + if (result.skippedActive) skippedActiveCount++; + } + } catch (error) { + addLog.warn('[Evaluation] Failed to cleanup data synthesize job', { jobId: job.id, error }); + } + } + + // 5. Clean orphaned summary jobs + for (const job of summaryJobs) { + try { + const { evalId } = job.data; + const evaluation = await MongoEvaluation.exists({ _id: evalId }); + + if (!evaluation) { + const result = await cleanupJob(job, 'summary', { evalId }); + if (result.cleaned) cleanedCount++; + if (result.skippedActive) skippedActiveCount++; + } + } catch (error) { + addLog.warn('[Evaluation] Failed to cleanup summary job', { jobId: job.id, error }); + } + } + + const result = { + totalJobs: + taskJobs.length + + itemJobs.length + + dataQualityJobs.length + + dataSynthesizeJobs.length + + summaryJobs.length, + cleanedJobs: cleanedCount, + skippedActiveJobs: skippedActiveCount, + breakdown: { + taskJobs: taskJobs.length, + itemJobs: itemJobs.length, + dataQualityJobs: dataQualityJobs.length, + dataSynthesizeJobs: dataSynthesizeJobs.length, + summaryJobs: summaryJobs.length + } + }; + + addLog.info('[Evaluation] Comprehensive orphaned jobs cleanup completed', result); + return result; + } catch (error) { + addLog.error('[Evaluation] Comprehensive orphaned jobs cleanup failed', { error }); + return null; + } +}; + +/** + * Helper function to cleanup individual job + */ +async function cleanupJob(job: any, jobType: string, context: Record) { + const jobState = await job.getState(); + + if (jobState === 'active') { + // Active job cannot be removed, log warning + addLog.warn(`[Evaluation] Found orphaned active ${jobType} job (cannot remove)`, { + jobId: job.id, + state: jobState, + ...context + }); + return { cleaned: false, skippedActive: true }; + } else { + // Non-active jobs can be safely removed + await job.remove(); + addLog.debug(`[Evaluation] Removed orphaned ${jobType} job`, { + jobId: job.id, + state: jobState, + ...context + }); + return { cleaned: true, skippedActive: false }; + } +} diff --git a/packages/service/core/evaluation/metric/provider.ts b/packages/service/core/evaluation/metric/provider.ts new file mode 100644 index 000000000000..9cdb23e8bfcb --- /dev/null +++ b/packages/service/core/evaluation/metric/provider.ts @@ -0,0 +1,45 @@ +import { readConfigData } from '../../../../../projects/app/src/service/common/system'; +import { EvalMetricTypeEnum } from '@fastgpt/global/core/evaluation/metric/constants'; +import type { EvalMetricSchemaType } from '@fastgpt/global/core/evaluation/metric/type'; + +let cachedBuiltinMetrics: EvalMetricSchemaType[] | null = null; +let loadingPromise: Promise | null = null; + +export async function getBuiltinMetrics(): Promise { + if (cachedBuiltinMetrics) { + return cachedBuiltinMetrics; + } + + if (loadingPromise) { + return loadingPromise; + } + + loadingPromise = loadBuiltinMetrics(); + + try { + const result = await loadingPromise; + cachedBuiltinMetrics = result; + return result; + } finally { + loadingPromise = null; + } +} + +async function loadBuiltinMetrics(): Promise { + const metricContent = await readConfigData('metric.json'); + const { builtinMetrics } = JSON.parse(metricContent); + + return (builtinMetrics || []).map((metric: any) => ({ + _id: `builtin_${metric.name}`, + teamId: '', + tmbId: '', + name: metric.name, + description: metric.description || '', + type: EvalMetricTypeEnum.Builtin, + createTime: new Date(), + updateTime: new Date(), + ...Object.fromEntries( + Object.entries(metric).filter(([key, value]) => key.endsWith('Required') && value === true) + ) + })) as EvalMetricSchemaType[]; +} diff --git a/packages/service/core/evaluation/metric/schema.ts b/packages/service/core/evaluation/metric/schema.ts index 2b0ce254a431..9b05293a89a1 100644 --- a/packages/service/core/evaluation/metric/schema.ts +++ b/packages/service/core/evaluation/metric/schema.ts @@ -23,11 +23,15 @@ const EvalMetricSchema = new Schema({ }, name: { type: String, - required: true + required: true, + trim: true, + maxlength: 100 }, description: { type: String, - required: false + default: '', + trim: true, + maxlength: 100 }, type: { type: String, @@ -41,34 +45,34 @@ const EvalMetricSchema = new Schema({ userInputRequired: { type: Boolean, - default: false + required: false }, actualOutputRequired: { type: Boolean, - default: false + required: false }, expectedOutputRequired: { type: Boolean, - default: false + required: false }, contextRequired: { type: Boolean, - default: false + required: false }, retrievalContextRequired: { type: Boolean, - default: false + required: false }, embeddingRequired: { type: Boolean, - default: false + required: false }, llmRequired: { type: Boolean, - default: false + required: false }, createTime: { @@ -85,6 +89,7 @@ const EvalMetricSchema = new Schema({ EvalMetricSchema.index({ teamId: 1, name: 1 }, { unique: true }); EvalMetricSchema.index({ createTime: -1 }); +EvalMetricSchema.index({ updateTime: -1 }); EvalMetricSchema.pre('save', function (next) { this.updateTime = new Date(); diff --git a/packages/service/core/evaluation/summary/index.ts b/packages/service/core/evaluation/summary/index.ts index 031a4f591bbd..93686222e34d 100644 --- a/packages/service/core/evaluation/summary/index.ts +++ b/packages/service/core/evaluation/summary/index.ts @@ -1,7 +1,6 @@ -import type { EvaluationStatusEnum } from '@fastgpt/global/core/evaluation/constants'; import { CaculateMethodMap, - EvaluationStatusEnum as EvalStatus, + EvaluationStatusEnum, CalculateMethodEnum } from '@fastgpt/global/core/evaluation/constants'; import { MongoEvaluation, MongoEvalItem } from '../task/schema'; @@ -14,18 +13,29 @@ import { addLog } from '../../../common/system/log'; import { SummaryStatusEnum, PERFECT_SCORE } from '@fastgpt/global/core/evaluation/constants'; import { getEvaluationSummaryTokenLimit } from '../utils/tokenLimiter'; import { createChatCompletion } from '../../ai/config'; -import { getLLMModel } from '../../ai/model'; +import { getLLMModel, getEvaluationModel } from '../../ai/model'; import { countGptMessagesTokens } from '../../../common/string/tiktoken'; import type { ChatCompletionMessageParam } from '@fastgpt/global/core/ai/type'; import { ChatCompletionRequestMessageRoleEnum } from '@fastgpt/global/core/ai/constants'; import { loadRequestMessages } from '../../chat/utils'; -import { evalSummaryTemplate, goodExample, badExample } from '@fastgpt/global/core/ai/prompt/eval'; +import { + problemAnalysisTemplate, + strengthAnalysisTemplate, + goodExample, + badExample +} from '@fastgpt/global/core/ai/prompt/eval'; import { checkTeamAIPoints } from '../../../support/permission/teamLimit'; import { addAuditLog } from '../../../support/user/audit/util'; import { AuditEventEnum } from '@fastgpt/global/support/user/audit/constants'; import { concatUsage, evaluationUsageIndexMap } from '../../../support/wallet/usage/controller'; import { createMergedEvaluationUsage } from '../utils/usage'; +import { formatModelChars2Points } from '../../../support/wallet/usage/utils'; +import { ModelTypeEnum } from '@fastgpt/global/core/ai/model'; import { EvaluationErrEnum } from '@fastgpt/global/common/error/code/evaluation'; +import { MetricResultStatusEnum } from '@fastgpt/global/core/evaluation/metric/constants'; +import { addSummaryTaskToQueue } from './queue'; +import { mongoSessionRun } from '../../../common/mongo/sessionRun'; +import type { ClientSession } from '../../../common/mongo'; export class EvaluationSummaryService { // Get evaluation summary report @@ -39,36 +49,52 @@ export class EvaluationSummaryService { errorReason?: string; completedItemCount: number; overThresholdItemCount: number; + underThresholdRate: number; + threshold: number; + weight: number; + customSummary: string; }>; aggregateScore: number; }> { - // Query evaluation task const evaluation = await MongoEvaluation.findById(evalId).lean(); + if (!evaluation) throw new Error(EvaluationErrEnum.evalTaskNotFound); - if (!evaluation) { - throw new Error(EvaluationErrEnum.evalTaskNotFound); - } - - // Real-time calculation of metricScore and aggregateScore + // Real-time calculate metric scores const calculatedData = await this.calculateMetricScores(evaluation); - // Build return data, merge calculation results and existing configurations + // Build return data using real-time calculated values const data = evaluation.evaluators.map((evaluator, index) => { const metricId = evaluator.metric._id.toString(); - const calculatedMetric = calculatedData.metricsData.find( - (item) => item.metricId === metricId - ); const summaryConfig = evaluation.summaryConfigs[index]; + // Find calculated metric data + const metricData = calculatedData.metricsData.find((m) => m.metricId === metricId); + const completedItemCount = metricData?.totalCount || 0; + const overThresholdItemCount = metricData?.aboveThresholdCount || 0; + const metricScore = metricData?.metricScore || 0; + + const threshold = evaluator.thresholdValue || 0; + const underThresholdRate = + completedItemCount > 0 + ? Math.round(((completedItemCount - overThresholdItemCount) / completedItemCount) * 100) + : 0; + const underThresholdItemCount = completedItemCount - overThresholdItemCount; + // Generate customSummary in format: "(完成个数个)summary" + const customSummary = `${underThresholdRate}%(${underThresholdItemCount}个)${summaryConfig.summary || ''}`; + return { metricId: metricId, metricName: evaluator.metric.name, - metricScore: calculatedMetric?.metricScore || 0, + metricScore: metricScore, // Use real-time calculated score summary: summaryConfig.summary, - summaryStatus: summaryConfig.summaryStatus.toString(), + summaryStatus: summaryConfig.summaryStatus, errorReason: summaryConfig.errorReason, - completedItemCount: calculatedMetric?.totalCount || 0, - overThresholdItemCount: calculatedMetric?.aboveThresholdCount || 0 + completedItemCount: completedItemCount, // Use real-time calculated count + overThresholdItemCount: overThresholdItemCount, // Use real-time calculated count + underThresholdRate: underThresholdRate, // Percentage of items that failed the threshold (0-100) + threshold: threshold, // Add threshold field + weight: summaryConfig.weight || 0, // Add weight field + customSummary: customSummary // Add customSummary field with specified format }; }); @@ -78,8 +104,23 @@ export class EvaluationSummaryService { }; } - // Real-time calculation of metricScore and aggregateScore - private static async calculateMetricScores(evaluation: EvaluationSchemaType): Promise<{ + // This method is no longer needed as scores are calculated in real-time + // Keeping for backward compatibility but will be deprecated + static async calculateAndSaveMetricScores( + evalId: string, + session?: any // 支持在事务中调用 + ): Promise { + addLog.warn( + '[Evaluation] calculateAndSaveMetricScores is deprecated, scores are now calculated in real-time', + { + evalId + } + ); + // No-op: scores are calculated in real-time when getEvaluationSummary is called + } + + // Real-time calculation of metricScore and aggregateScore (pure calculation, no database updates) + static async calculateMetricScores(evaluation: EvaluationSchemaType): Promise<{ metricsData: Array<{ metricId: string; metricName: string; @@ -87,7 +128,6 @@ export class EvaluationSummaryService { weight: number; thresholdValue: number; aboveThresholdCount: number; - thresholdPassRate: number; totalCount: number; }>; aggregateScore: number; @@ -95,37 +135,104 @@ export class EvaluationSummaryService { try { const evalId = new Types.ObjectId(evaluation._id); - // MongoDB aggregation pipeline - compatible with older MongoDB versions + // Check if evaluation has required fields + if (!evaluation.evaluators || !Array.isArray(evaluation.evaluators)) { + addLog.warn('[calculateMetricScores] Evaluation has no evaluators', { + evalId: evaluation._id + }); + return { + metricsData: [], + aggregateScore: 0 + }; + } + + if (!evaluation.summaryConfigs || !Array.isArray(evaluation.summaryConfigs)) { + addLog.warn('[calculateMetricScores] Evaluation has no summaryConfigs', { + evalId: evaluation._id + }); + return { + metricsData: [], + aggregateScore: 0 + }; + } + + // MongoDB aggregation pipeline - Calculate both successful scores and total completed count per metric const pipeline = [ - // Step 1: Filter successful evaluation items + // Step 1: Filter evaluation items that have evaluator outputs { $match: { evalId: evalId, - status: EvalStatus.completed, - 'evaluatorOutput.data.score': { $exists: true, $ne: null } + evaluatorOutputs: { $exists: true, $nin: [null, []] } } }, - // Step 2: Group by metric ID and calculate statistics + // Step 2: Add fields to find matching metric results + { + $addFields: { + // Create an array of metric results for easier processing + metricResults: { + $map: { + input: '$evaluatorOutputs', + as: 'output', + in: { + metricName: '$$output.metricName', + status: '$$output.status', + score: '$$output.data.score', + hasValidScore: { + $and: [ + { $eq: ['$$output.status', MetricResultStatusEnum.Success] }, + { $ne: ['$$output.data.score', null] } + ] + } + } + } + } + } + }, + // Step 3: Unwind the metric results + { + $unwind: '$metricResults' + }, + // Step 4: Group by metric name and calculate statistics { $group: { - _id: '$evaluatorOutput.metricName', - scores: { $push: '$evaluatorOutput.data.score' }, - avgScore: { $avg: '$evaluatorOutput.data.score' }, - count: { $sum: 1 }, - metricName: { $first: '$evaluatorOutput.metricName' } + _id: '$metricResults.metricName', + // Collect all valid scores for this metric + successfulScores: { + $push: { + $cond: ['$metricResults.hasValidScore', '$metricResults.score', '$$REMOVE'] + } + }, + // Count unique evaluation items that have this metric (regardless of success/failure) + uniqueItemIds: { + $addToSet: '$_id' + }, + metricName: { $first: '$metricResults.metricName' } + } + }, + // Step 5: Calculate final statistics + { + $addFields: { + avgScore: { + $cond: [ + { $gt: [{ $size: '$successfulScores' }, 0] }, + { $avg: '$successfulScores' }, + 0 + ] + }, + successCount: { $size: '$successfulScores' }, + totalCompletedCount: { $size: '$uniqueItemIds' } } } ]; const metricsStats = await MongoEvalItem.aggregate(pipeline as any); - addLog.info(`mongo实时计算结果为${JSON.stringify(metricsStats)}`); // Calculate median for each statistic (since different evaluators may have different calculation methods) const processedStats = metricsStats.map((stats) => { let medianScore = 0; - if (stats.scores && stats.scores.length > 0) { - const sortedScores = [...stats.scores].sort((a, b) => a - b); + if (stats.successfulScores && stats.successfulScores.length > 0) { + const sortedScores = [...stats.successfulScores].sort((a, b) => a - b); const length = sortedScores.length; if (length % 2 === 0) { @@ -153,7 +260,6 @@ export class EvaluationSummaryService { weight: number; thresholdValue: number; aboveThresholdCount: number; - thresholdPassRate: number; totalCount: number; }> = []; @@ -167,20 +273,24 @@ export class EvaluationSummaryService { const summaryConfig = evaluation.summaryConfigs[index]; if (stats) { - // Select score based on current evaluator's calculation method - const metricScore = + // Select score based on current evaluator's calculation method with NaN protection + let rawScore = summaryConfig.calculateType === CalculateMethodEnum.median - ? Math.round(stats.medianScore * 100) / 100 - : Math.round(stats.avgScore * 100) / 100; + ? stats.medianScore + : stats.avgScore; + + // Ensure score is valid number + if (isNaN(rawScore) || rawScore === null || rawScore === undefined) { + rawScore = 0; + } + + const metricScore = Math.round(rawScore * 100) / 100; - // Calculate threshold statistics - const aboveThresholdCount = stats.scores.filter( + // Calculate threshold statistics - count successful scores that meet threshold + const aboveThresholdCount = stats.successfulScores.filter( (score: number) => score >= (evaluator.thresholdValue || 0) ).length; - const thresholdPassRate = - stats.count > 0 ? Math.round((aboveThresholdCount / stats.count) * 10000) / 100 : 0; - const weight = summaryConfig.weight; metricsData.push({ @@ -190,8 +300,7 @@ export class EvaluationSummaryService { weight, thresholdValue: evaluator.thresholdValue || 0, aboveThresholdCount, - thresholdPassRate, - totalCount: stats.count + totalCount: stats.totalCompletedCount }); // Accumulate weighted scores @@ -199,28 +308,30 @@ export class EvaluationSummaryService { totalWeight += weight; } else { // Metrics with no data + const weight = summaryConfig.weight; + metricsData.push({ metricId: metricId, metricName: evaluator.metric.name, metricScore: 0, - weight: summaryConfig.weight, + weight: weight, thresholdValue: evaluator.thresholdValue || 0, aboveThresholdCount: 0, - thresholdPassRate: 0, totalCount: 0 }); + + // Accumulate weighted scores for metrics with no data (score = 0) + totalWeightedScore += 0 * weight; + totalWeight += weight; } }); - // Calculate aggregate score - const aggregateScore = - totalWeight > 0 ? Math.round((totalWeightedScore / totalWeight) * 100) / 100 : 0; - - addLog.info('[Evaluation] Real-time calculation completed', { - evalId: evaluation._id.toString(), - metricsCount: metricsData.length, - aggregateScore - }); + // Calculate aggregate score with NaN protection + let aggregateScore = 0; + if (totalWeight > 0 && !isNaN(totalWeightedScore) && !isNaN(totalWeight)) { + const rawScore = totalWeightedScore / totalWeight; + aggregateScore = isNaN(rawScore) ? 0 : Math.round(rawScore * 100) / 100; + } return { metricsData, @@ -242,7 +353,6 @@ export class EvaluationSummaryService { weight: summaryConfig.weight, thresholdValue: evaluator.thresholdValue || 0, aboveThresholdCount: 0, - thresholdPassRate: 0, totalCount: 0 }; }); @@ -255,6 +365,7 @@ export class EvaluationSummaryService { } // Update evaluation summary configuration (threshold, weight, calculation method) + // 使用MongoDB事务保证配置更新的原子性 static async updateEvaluationSummaryConfig( evalId: string, metricsConfig: Array<{ @@ -264,6 +375,12 @@ export class EvaluationSummaryService { calculateType?: CalculateMethodEnum; }> ): Promise { + addLog.info('[Evaluation] Starting configuration update', { + evalId, + metricsCount: metricsConfig.length + }); + + // 检查基本参数有效性 const evaluation = await MongoEvaluation.findById(evalId).lean(); if (!evaluation) throw new Error(EvaluationErrEnum.evalTaskNotFound); @@ -276,80 +393,50 @@ export class EvaluationSummaryService { } } - // Update configuration based on existing evaluators (threshold, weight, calculation method) - const configMap = new Map( - metricsConfig.map((m) => [ - m.metricId, - { thresholdValue: m.thresholdValue, weight: m.weight, calculateType: m.calculateType } - ]) - ); + // 使用事务更新配置 + await mongoSessionRun(async (session: ClientSession) => { + const configMap = new Map(metricsConfig.map((m) => [m.metricId, m])); - // Update corresponding configuration in evaluators array and summaryConfigs - const updatedEvaluators = (evaluation.evaluators || []).map((evaluator: any) => { - const metricId = evaluator.metric._id.toString(); - const config = configMap.get(metricId); - if (config) { - return { - ...evaluator, - thresholdValue: config.thresholdValue - }; - } - return evaluator; - }); + // 更新evaluators + const updatedEvaluators = evaluation.evaluators.map((evaluator: any) => { + const config = configMap.get(evaluator.metric._id.toString()); + return config ? { ...evaluator, thresholdValue: config.thresholdValue } : evaluator; + }); - // Update summaryConfigs array - const updatedSummaryConfigs = evaluation.summaryConfigs.map( - (summaryConfig: any, index: number) => { - const evaluator = evaluation.evaluators[index]; - const metricId = evaluator.metric._id.toString(); - const config = configMap.get(metricId); - - if (config) { - return { - ...summaryConfig, - ...(config.weight !== undefined ? { weight: config.weight } : {}), - ...(config.calculateType !== undefined ? { calculateType: config.calculateType } : {}) - }; + // 更新summaryConfigs + const updatedSummaryConfigs = evaluation.summaryConfigs.map( + (summaryConfig: any, index: number) => { + const metricId = evaluation.evaluators[index].metric._id.toString(); + const config = configMap.get(metricId); + + if (config) { + return { + ...summaryConfig, + ...(config.weight !== undefined && { weight: config.weight }), + ...(config.calculateType !== undefined && { calculateType: config.calculateType }) + }; + } + return summaryConfig; } - return summaryConfig; - } - ); + ); - // Update evaluation configuration - await MongoEvaluation.updateOne( - { _id: evalId }, - { - $set: { - evaluators: updatedEvaluators, - summaryConfigs: updatedSummaryConfigs - } - } - ); + await MongoEvaluation.updateOne( + { _id: evalId }, + { $set: { evaluators: updatedEvaluators, summaryConfigs: updatedSummaryConfigs } }, + { session } + ); - // Update threshold values in all related eval_items - const thresholdUpdates = metricsConfig.filter((config) => config.thresholdValue !== undefined); - if (thresholdUpdates.length > 0) { - for (const config of thresholdUpdates) { - const updateResult = await MongoEvalItem.updateMany( - { - evalId: evalId, - 'evaluator.metric._id': config.metricId - }, - { - $set: { - 'evaluator.thresholdValue': config.thresholdValue - } - } - ); + addLog.info('[Evaluation] Configuration updated successfully', { + evalId, + evaluatorsCount: updatedEvaluators.length, + summaryConfigsCount: updatedSummaryConfigs.length + }); + }); - addLog.info('[Evaluation] Updated threshold in eval_items', { - evalId, - metricId: config.metricId, - newThreshold: config.thresholdValue, - updatedCount: updateResult.modifiedCount - }); - } - } + addLog.info('[Evaluation] Configuration update completed successfully', { + evalId, + metricsCount: metricsConfig.length + }); } // Get evaluation summary configuration details @@ -359,16 +446,13 @@ export class EvaluationSummaryService { metricsConfig: Array<{ metricId: string; metricName: string; + metricDescription: string; thresholdValue: number; weight: number; }>; }> { - // Query evaluation task const evaluation = await MongoEvaluation.findById(evalId).lean(); - - if (!evaluation) { - throw new Error(EvaluationErrEnum.evalTaskNotFound); - } + if (!evaluation) throw new Error(EvaluationErrEnum.evalTaskNotFound); // Get calculation type from first summary config (since all metrics use the same type) const firstSummaryConfig = evaluation.summaryConfigs[0]; @@ -381,6 +465,7 @@ export class EvaluationSummaryService { return { metricId: evaluator.metric._id.toString(), metricName: evaluator.metric.name, + metricDescription: evaluator.metric.description || '', thresholdValue: evaluator.thresholdValue || 0, weight: summaryConfig.weight }; @@ -414,7 +499,7 @@ export class EvaluationSummaryService { // ===== Summary Generation Methods ===== /** - * 生成多个指标的总结报告 - 异步触发,立即返回 + * 生成多个指标的总结报告 - 使用BullMQ队列化处理,解决系统崩溃导致状态卡死问题 */ static async generateSummaryReports(evalId: string, metricIds: string[]): Promise { try { @@ -434,12 +519,7 @@ export class EvaluationSummaryService { ); // Validate metric ownership and find corresponding evaluator index - const evaluatorTasks: Array<{ - metricId: string; - evaluatorIndex: number; - evaluator: any; - }> = []; - + const validMetricIds: string[] = []; const skippedMetrics: Array<{ metricId: string; metricName: string; @@ -464,29 +544,10 @@ export class EvaluationSummaryService { return; } - // 检查该指标是否已经在生成中 - const summaryConfig = evaluation.summaryConfigs[evaluatorIndex]; - if (summaryConfig.summaryStatus === SummaryStatusEnum.generating) { - const metricName = evaluation.evaluators[evaluatorIndex].metric.name; - addLog.info('[EvaluationSummary] Metric is already generating, skipping', { - evalId, - metricId, - metricName, - currentStatus: summaryConfig.summaryStatus - }); - skippedMetrics.push({ - metricId, - metricName, - reason: 'Already generating' - }); - return; - } + // Get metric name for logging + const metricName = evaluation.evaluators[evaluatorIndex].metric.name; - evaluatorTasks.push({ - metricId, - evaluatorIndex, - evaluator: evaluation.evaluators[evaluatorIndex] - }); + validMetricIds.push(metricId); }); // 记录跳过的指标信息 @@ -502,7 +563,7 @@ export class EvaluationSummaryService { }); } - if (evaluatorTasks.length === 0) { + if (validMetricIds.length === 0) { if (skippedMetrics.length > 0) { addLog.info('[EvaluationSummary] All metrics were skipped, no tasks to execute', { evalId, @@ -514,29 +575,14 @@ export class EvaluationSummaryService { throw new Error(EvaluationErrEnum.summaryNoValidMetricsFound); } - // Immediately update all related evaluator status to generating (batch update) - const updateFields: Record = {}; - evaluatorTasks.forEach((task) => { - updateFields[`summaryConfigs.${task.evaluatorIndex}.summaryStatus`] = - SummaryStatusEnum.generating; - }); - - await MongoEvaluation.updateOne({ _id: evalId }, { $set: updateFields }); + // 将任务添加到BullMQ队列中,让队列负责状态管理 + await addSummaryTaskToQueue(evalId, validMetricIds); - addLog.info('[EvaluationSummary] Status updated to generating, starting async processing', { + addLog.info('[EvaluationSummary] Task successfully added to queue', { evalId, totalRequested: metricIds.length, - validMetricsCount: evaluatorTasks.length, - skippedCount: skippedMetrics.length, - validMetrics: evaluatorTasks.map((task) => ({ - metricId: task.metricId, - metricName: task.evaluator.metric.name - })) - }); - - // Execute report generation asynchronously, don't wait for results - setImmediate(() => { - this.executeAsyncSummaryGeneration(evaluation, evaluatorTasks); + validMetricsCount: validMetricIds.length, + skippedCount: skippedMetrics.length }); } catch (error) { addLog.error('[EvaluationSummary] Report generation task creation failed', { @@ -548,60 +594,10 @@ export class EvaluationSummaryService { } } - /** - * 异步执行报告生成 - 后台处理 - */ - private static async executeAsyncSummaryGeneration( - evaluation: EvaluationSchemaType, - evaluatorTasks: Array<{ - metricId: string; - evaluatorIndex: number; - evaluator: any; - }> - ): Promise { - const evalId = evaluation._id.toString(); - - addLog.info('[EvaluationSummary] Starting async concurrent report generation', { - evalId, - totalTasks: evaluatorTasks.length - }); - - try { - // Generate reports concurrently - await Promise.all( - evaluatorTasks.map((task) => - this.generateSingleMetricSummary( - evaluation, - task.metricId, - task.evaluatorIndex, - task.evaluator - ).catch((error) => { - addLog.error('[EvaluationSummary] Single metric report generation failed', { - evalId, - metricId: task.metricId, - error - }); - // Don't block other metrics generation - }) - ) - ); - - addLog.info('[EvaluationSummary] Async concurrent report generation completed', { - evalId, - completedCount: evaluatorTasks.length - }); - } catch (error) { - addLog.error('[EvaluationSummary] Error occurred during async report generation', { - evalId, - error - }); - } - } - /** * 生成单个指标的总结报告 */ - private static async generateSingleMetricSummary( + static async generateSingleMetricSummary( evaluation: EvaluationSchemaType, metricId: string, evaluatorIndex: number, @@ -620,7 +616,8 @@ export class EvaluationSummaryService { const { filteredData, totalDataCount } = await this.getFilteredEvaluationData( evalId, metricId, - evaluator.thresholdValue || 0 + evaluator.thresholdValue || 0, + evaluator ); if (filteredData.length === 0) { @@ -628,13 +625,7 @@ export class EvaluationSummaryService { evalId, metricId }); - await this.updateSummaryResult( - evalId, - evaluatorIndex, - SummaryStatusEnum.failed, - 'No matching evaluation data found, cannot generate summary report' - ); - return; + throw new Error('No matching evaluation data found, cannot generate summary report'); } // 2. Token control and content preparation @@ -731,24 +722,6 @@ export class EvaluationSummaryService { } } - /** - * 更新摘要状态 - */ - private static async updateSummaryStatus( - evalId: string, - evaluatorIndex: number, - status: SummaryStatusEnum - ): Promise { - await MongoEvaluation.updateOne( - { _id: evalId }, - { - $set: { - [`summaryConfigs.${evaluatorIndex}.summaryStatus`]: status - } - } - ); - } - /** * 更新摘要结果 */ @@ -779,7 +752,8 @@ export class EvaluationSummaryService { private static async getFilteredEvaluationData( evalId: string, metricId: string, - thresholdValue: number + thresholdValue: number, + evaluator: any ): Promise<{ filteredData: any[]; totalDataCount: number; @@ -787,30 +761,57 @@ export class EvaluationSummaryService { addLog.debug('[getFilteredEvaluationData] 入参:', { evalId, metricId, + metricName: evaluator.metric.name, thresholdValue }); - try { - // Process evalId, ensure correct ObjectId format - const evalObjectId = - typeof evalId === 'string' && evalId.length === 24 ? new Types.ObjectId(evalId) : evalId; + // Get evaluation to check metric count + const evaluation = await MongoEvaluation.findById(evalId).lean(); + if (!evaluation) { + throw new Error('Evaluation not found'); + } // Query successfully completed evaluation items for specific metric, sorted by score (low priority) // Note: evaluator.metric._id is stored as string, not ObjectId const pipeline = [ { $match: { - evalId: evalObjectId, - 'evaluator.metric._id': metricId, - 'evaluatorOutput.data.score': { $exists: true, $ne: null }, - status: EvalStatus.completed + evalId: new Types.ObjectId(evalId), + evaluatorOutputs: { $exists: true, $nin: [null, []] } } }, { $addFields: { - score: '$evaluatorOutput.data.score', + // Find the matching metric result in evaluatorOutputs array + matchingMetricResult: { + $arrayElemAt: [ + { + $filter: { + input: '$evaluatorOutputs', + as: 'output', + cond: { + $and: [ + { $eq: ['$$output.metricName', evaluator.metric.name] }, + { $eq: ['$$output.status', MetricResultStatusEnum.Success] } + ] + } + } + }, + 0 + ] + } + } + }, + { + $match: { + 'matchingMetricResult.data.score': { $exists: true, $ne: null } + } + }, + { + $addFields: { + score: '$matchingMetricResult.data.score', isBelowThreshold: { - $lt: ['$evaluatorOutput.data.score', thresholdValue] + $lt: ['$matchingMetricResult.data.score', thresholdValue] } } }, @@ -824,7 +825,8 @@ export class EvaluationSummaryService { $project: { dataItem: 1, targetOutput: 1, - evaluatorOutput: 1, + evaluatorOutputs: 1, + matchingMetricResult: 1, score: 1, isBelowThreshold: 1 } @@ -879,7 +881,8 @@ export class EvaluationSummaryService { const selectedExample = isAllPerfect ? goodExample : badExample; // Calculate base template tokens (excluding specific data) - const baseTemplate = evalSummaryTemplate + const selectedTemplate = isAllPerfect ? strengthAnalysisTemplate : problemAnalysisTemplate; + const baseTemplate = selectedTemplate .replace('{example}', selectedExample) .replace('{evaluation_result_for_single_metric}', ''); @@ -948,7 +951,7 @@ export class EvaluationSummaryService { }> { try { const llmModel = evaluator.runtimeConfig?.llm; - const modelData = llmModel ? getLLMModel(llmModel) : getLLMModel(); + const modelData = llmModel ? getLLMModel(llmModel) : getEvaluationModel() || getLLMModel(); const userPrompt = this.buildUserPrompt(data); @@ -969,18 +972,12 @@ export class EvaluationSummaryService { body: { model: llmModel, messages: requestMessages, - temperature: 0.3, + temperature: 1e-7, max_tokens: 1000, stream: false }, modelData }); - addLog.info('[EvaluationSummary] LLM request messages', { - messages: JSON.stringify(messages, null, 2) - }); - addLog.info('[EvaluationSummary] LLM response', { - response: JSON.stringify(response, null, 2) - }); if (isStreamResponse) { throw new Error(EvaluationErrEnum.summaryStreamResponseNotSupported); @@ -989,14 +986,6 @@ export class EvaluationSummaryService { const summary = response.choices[0]?.message?.content || '生成总结失败'; const usage = response.usage; - addLog.info('[EvaluationSummary] Extracted summary', { - summary: summary, - summaryLength: summary.length - }); - addLog.info('[EvaluationSummary] Extracted usage', { - usage: usage - }); - return { summary, usage }; } catch (error) { addLog.error('[EvaluationSummary] LLM call failed', { @@ -1018,19 +1007,18 @@ export class EvaluationSummaryService { if (!usage) return; try { - const modelData = llmModel ? getLLMModel(llmModel) : getLLMModel(); + const modelData = llmModel ? getLLMModel(llmModel) : getEvaluationModel() || getLLMModel(); const inputTokens = usage?.prompt_tokens || 0; const outputTokens = usage?.completion_tokens || 0; const totalTokens = inputTokens + outputTokens; - // Convert tokens to points - const totalPoints = modelData - ? Math.ceil( - (inputTokens * (modelData.inputPrice || 0) + - outputTokens * (modelData.outputPrice || 0)) / - 1000 - ) - : 0; + // Convert tokens to points using standard utility function + const { totalPoints } = formatModelChars2Points({ + model: llmModel || modelData?.model || '', + inputTokens, + outputTokens, + modelType: (modelData?.type as `${ModelTypeEnum}`) || ModelTypeEnum.llm + }); // Use unified evaluation usage recording await createMergedEvaluationUsage({ @@ -1064,14 +1052,18 @@ export class EvaluationSummaryService { * 构建用户提示词 */ private static buildUserPrompt(data: any[]): string { - // Select appropriate example type - const selectedExample = this.selectExampleType(data); + // Check if all scores are perfect to determine which template to use + const isAllPerfect = this.isAllPerfectScores(data); + + // Select appropriate template and example based on data quality + const selectedTemplate = isAllPerfect ? strengthAnalysisTemplate : problemAnalysisTemplate; + const selectedExample = isAllPerfect ? goodExample : badExample; // Format evaluation data const evaluationResult = data.map((item) => this.formatDataItemForPrompt(item)).join('\n\n'); // Render template variables - return evalSummaryTemplate + return selectedTemplate .replace('{example}', selectedExample) .replace('{evaluation_result_for_single_metric}', evaluationResult); } @@ -1080,14 +1072,14 @@ export class EvaluationSummaryService { * 格式化数据项用于提示词 */ private static formatDataItemForPrompt(item: any): string { - // const score = item.evaluatorOutput?.data?.score || 0; + // const score = item.matchingMetricResult?.data?.score || 0; // const userInput = item.dataItem?.userInput || '无'; // const expectedOutput = item.dataItem?.expectedOutput || '无'; // const actualOutput = item.targetOutput?.actualOutput || '无'; - const reason = item.evaluatorOutput?.data?.reason || '无评估原因'; + const reason = item.matchingMetricResult?.data?.reason || '无评估原因'; return ` -**评估原因**: ${reason}`; +评估原因: ${reason}`; } /** @@ -1095,7 +1087,7 @@ export class EvaluationSummaryService { */ private static isAllPerfectScores(data: any[]): boolean { if (data.length === 0) return false; - return data.every((item) => (item.evaluatorOutput?.data?.score || 0) >= PERFECT_SCORE); + return data.every((item) => (item.matchingMetricResult?.data?.score || 0) >= PERFECT_SCORE); } /** @@ -1128,10 +1120,92 @@ export class EvaluationSummaryService { } else { // When non-perfect scores exist, prioritize non-perfect score data const nonPerfectData = data.filter( - (item) => (item.evaluatorOutput?.data?.score || 0) < PERFECT_SCORE + (item) => (item.matchingMetricResult?.data?.score || 0) < PERFECT_SCORE ); // Return non-perfect score data first (sorted by score from low to high) return [...nonPerfectData]; } } + + /** + * Trigger summary generation for completed evaluation task + */ + static async triggerSummaryGeneration(evalId: string, completedCount: number): Promise { + try { + // Check if all evaluation items have error status - skip summary generation if true + const allEvalItemsStatus = await MongoEvalItem.find( + { evalId: new Types.ObjectId(evalId) }, + { 'metadata.status': 1 } + ).lean(); + + const allItemsAbnormal = + allEvalItemsStatus.length > 0 && + allEvalItemsStatus.every((item) => item.metadata?.status === EvaluationStatusEnum.error); + + if (allItemsAbnormal) { + addLog.warn( + '[Evaluation] All evaluation items have error status, skipping summary generation for all metrics', + { + evalId, + totalItems: allEvalItemsStatus.length + } + ); + return; // Skip summary generation entirely + } + + // Check if there are any successful evaluatorOutputs, regardless of overall item status + const itemsWithSuccessfulOutputs = await MongoEvalItem.countDocuments({ + evalId: new Types.ObjectId(evalId), + evaluatorOutputs: { + $elemMatch: { + status: MetricResultStatusEnum.Success, + 'data.score': { $exists: true, $ne: null } + } + } + }); + + if (completedCount === 0 && itemsWithSuccessfulOutputs === 0) { + return; // No successful items, skip summary generation + } + // Scores are now calculated in real-time when getEvaluationSummary is called + // No need to pre-calculate and save scores + + // Check which metrics need summary generation + const currentEvaluation = await MongoEvaluation.findById( + evalId, + 'evaluators summaryConfigs' + ).lean(); + + if (!currentEvaluation?.evaluators || currentEvaluation.evaluators.length === 0) { + return; // No evaluators to process + } + + // Find metrics with empty summaries + const metricsNeedingSummary: string[] = []; + + currentEvaluation.evaluators.forEach((evaluator: any, index: number) => { + const metricId = evaluator.metric._id.toString(); + const summaryConfig = currentEvaluation.summaryConfigs[index]; + + // Check if summary is empty + if (!summaryConfig?.summary || summaryConfig.summary.trim() === '') { + metricsNeedingSummary.push(metricId); + } + }); + + if (metricsNeedingSummary.length > 0) { + // Trigger async summary generation for metrics with empty summaries + await EvaluationSummaryService.generateSummaryReports(evalId, metricsNeedingSummary); + } else { + addLog.debug( + `[Evaluation] All metrics already have summaries, skipping summary generation: ${evalId}` + ); + } + } catch (error) { + // Log error without affecting main completion flow + addLog.warn(`[Evaluation] Failed to trigger summary generation: ${evalId}`, { + error + }); + } + } } diff --git a/packages/service/core/evaluation/summary/queue.ts b/packages/service/core/evaluation/summary/queue.ts new file mode 100644 index 000000000000..c9de5fe613b3 --- /dev/null +++ b/packages/service/core/evaluation/summary/queue.ts @@ -0,0 +1,116 @@ +import { getQueue } from '../../../common/bullmq'; +import { QueueNames } from '../../../common/bullmq'; +import { addLog } from '../../../common/system/log'; +import { SummaryStatusHandler } from './statusHandler'; +import { SummaryStatusEnum } from '@fastgpt/global/core/evaluation/constants'; + +// 评估总结任务数据接口 +export interface EvaluationSummaryJobData { + evalId: string; + metricId: string; + timestamp: number; +} + +// 获取评估总结队列 +export function getEvaluationSummaryQueue() { + return getQueue(QueueNames.evaluationSummary); +} + +// 检查是否有运行中的 summary 任务 +async function checkActiveSummaryJob(evalId: string, metricId: string): Promise { + try { + const queue = getEvaluationSummaryQueue(); + + // 获取所有运行中的任务 + const activeJobs = await queue.getJobs(['active', 'waiting', 'delayed', 'prioritized']); + + // 检查是否有匹配的任务 + const existingJob = activeJobs.find( + (job) => job.data.evalId === evalId && job.data.metricId === metricId + ); + + return !!existingJob; + } catch (error) { + addLog.error('[EvaluationSummary] Failed to check active summary job', { + evalId, + metricId, + error + }); + return false; // 检查失败时假设没有运行中的任务 + } +} + +// 添加评估总结任务到队列 +export async function addSummaryTaskToQueue(evalId: string, metricIds: string[]): Promise { + try { + const queue = getEvaluationSummaryQueue(); + + // 为每个metricId创建单独的job + const addPromises = metricIds.map(async (metricId) => { + // 检查是否已有运行中的任务 + const hasActiveJob = await checkActiveSummaryJob(evalId, metricId); + if (hasActiveJob) { + addLog.warn('[EvaluationSummary] Task already in progress, skipping', { + evalId, + metricId + }); + return null; // 跳过重复任务 + } + + // 设置 pending 状态 + await SummaryStatusHandler.updateStatus( + evalId, + metricId, + SummaryStatusEnum.pending, + undefined, + new Date() + ); + + addLog.info('[EvaluationSummary] Adding new task to queue', { + evalId, + metricId + }); + + // 参考 taskitem 的写法:不指定 jobId,使用 deduplication 去重 + return queue.add( + 'generateSummary', + { + evalId, + metricId, + timestamp: Date.now() + }, + { + attempts: 1, // 不自动重试,由用户通过API主动重试 + removeOnComplete: { + count: 100 // 保留最近100个完成的任务,便于查看历史 + }, + removeOnFail: { + count: 50 // 保留最近50个失败的任务,便于调试 + }, + deduplication: { + id: `${evalId}_${metricId}`, // 使用 evalId+metricId 去重 + ttl: 5000 // 5秒内防止重复提交 + } + } + ); + }); + + const results = await Promise.all(addPromises); + const successfullyAdded = results.filter(Boolean).length; + const skipped = metricIds.length - successfullyAdded; + + addLog.info('[EvaluationSummary] Task successfully added to queue', { + evalId, + totalRequested: metricIds.length, + validMetricsCount: successfullyAdded, + skippedCount: skipped + }); + } catch (error) { + addLog.error('[EvaluationSummary] Failed to add tasks to queue', { + evalId, + metricIds, + error + }); + throw error; + } +} diff --git a/packages/service/core/evaluation/summary/statusHandler.ts b/packages/service/core/evaluation/summary/statusHandler.ts new file mode 100644 index 000000000000..3469dd1007d5 --- /dev/null +++ b/packages/service/core/evaluation/summary/statusHandler.ts @@ -0,0 +1,143 @@ +import { MongoEvaluation } from '../task/schema'; +import { SummaryStatusEnum } from '@fastgpt/global/core/evaluation/constants'; +import { addLog } from '../../../common/system/log'; + +// 状态更新处理器 +export class SummaryStatusHandler { + /** + * 更新评估总结状态 + * @param evalId 评估任务ID + * @param metricId 指标ID + * @param status 新状态 + * @param errorReason 错误原因(可选) + * @param timestamp 时间戳(可选) + */ + static async updateStatus( + evalId: string, + metricId: string, + status: SummaryStatusEnum, + errorReason?: string, + timestamp?: Date + ): Promise { + try { + const evaluation = await MongoEvaluation.findById(evalId).lean(); + if (!evaluation) { + addLog.warn('[SummaryStatusHandler] Evaluation not found', { evalId, metricId }); + return false; + } + + const evaluatorIndex = evaluation.evaluators.findIndex( + (evaluator: any) => evaluator.metric._id.toString() === metricId + ); + + if (evaluatorIndex === -1) { + addLog.warn('[SummaryStatusHandler] Metric not found in evaluation', { evalId, metricId }); + return false; + } + + // 构建更新对象 + const updateObj: Record = { + [`summaryConfigs.${evaluatorIndex}.summaryStatus`]: status + }; + + // 根据状态设置不同的字段 + switch (status) { + case SummaryStatusEnum.generating: + updateObj[`summaryConfigs.${evaluatorIndex}.errorReason`] = ''; + break; + + case SummaryStatusEnum.completed: + updateObj[`summaryConfigs.${evaluatorIndex}.errorReason`] = ''; + break; + + case SummaryStatusEnum.failed: + updateObj[`summaryConfigs.${evaluatorIndex}.errorReason`] = + errorReason || 'Unknown error'; + break; + } + + await MongoEvaluation.updateOne({ _id: evalId }, { $set: updateObj }); + + addLog.info('[SummaryStatusHandler] Status updated successfully', { + evalId, + metricId, + status, + errorReason, + evaluatorIndex, + timestamp: timestamp || new Date() + }); + + return true; + } catch (error) { + addLog.error('[SummaryStatusHandler] Failed to update status', { + evalId, + metricId, + status, + error + }); + return false; + } + } + + /** + * 批量更新多个指标的状态 + * @param evalId 评估任务ID + * @param updates 更新列表 + */ + static async batchUpdateStatus( + evalId: string, + updates: Array<{ + metricId: string; + status: SummaryStatusEnum; + errorReason?: string; + timestamp?: Date; + }> + ): Promise { + const results = await Promise.allSettled( + updates.map((update) => + this.updateStatus( + evalId, + update.metricId, + update.status, + update.errorReason, + update.timestamp + ) + ) + ); + + return results.map((result) => result.status === 'fulfilled' && result.value); + } + + /** + * 获取指标的当前状态 + * @param evalId 评估任务ID + * @param metricId 指标ID + */ + static async getStatus(evalId: string, metricId: string): Promise { + try { + const evaluation = await MongoEvaluation.findById(evalId).lean(); + if (!evaluation) { + return null; + } + + const evaluatorIndex = evaluation.evaluators.findIndex( + (evaluator: any) => evaluator.metric._id.toString() === metricId + ); + + if (evaluatorIndex === -1) { + return null; + } + + return ( + evaluation.summaryConfigs?.[evaluatorIndex]?.summaryStatus || SummaryStatusEnum.pending + ); + } catch (error) { + addLog.error('[SummaryStatusHandler] Failed to get status', { + evalId, + metricId, + error + }); + return null; + } + } +} diff --git a/packages/service/core/evaluation/summary/util/weightCalculator.ts b/packages/service/core/evaluation/summary/util/weightCalculator.ts index 0d77815d8da8..3a01e4ebdff6 100644 --- a/packages/service/core/evaluation/summary/util/weightCalculator.ts +++ b/packages/service/core/evaluation/summary/util/weightCalculator.ts @@ -37,15 +37,13 @@ export function calculateMetricWeights(metricCount: number): number[] { * @returns Default threshold value */ function getDefaultThreshold(): number { - const threshold = process.env.EVALUATION_DEFAULT_THRESHOLD - ? Number(process.env.EVALUATION_DEFAULT_THRESHOLD) - : 80; + const threshold = global.systemEnv?.evalConfig?.caseResultThreshold || 0.8; // Validate threshold is within valid range - if (isNaN(threshold) || threshold < 0 || threshold > 100) { + if (isNaN(threshold) || threshold < 0 || threshold > 1) { addLog.warn( - `[getDefaultThreshold] Invalid EVALUATION_DEFAULT_THRESHOLD value: ${process.env.EVALUATION_DEFAULT_THRESHOLD}. Using default: 80` + `[getDefaultThreshold] Invalid caseResultThreshold value: ${threshold}. Using default: 0.8` ); - return 80; + return 0.8; } return threshold; } @@ -63,13 +61,13 @@ export function buildEvalDataConfig(evaluators: EvaluatorSchema[]): { } const weights = calculateMetricWeights(evaluators.length); - const defaultThreshold = getDefaultThreshold(); + const caseResultThreshold = getDefaultThreshold(); // Clean evaluators without summaryConfig const cleanedEvaluators = evaluators.map((evaluator) => ({ metric: evaluator.metric, runtimeConfig: evaluator.runtimeConfig, - thresholdValue: evaluator.thresholdValue ?? defaultThreshold + thresholdValue: evaluator.thresholdValue ?? caseResultThreshold })); // Create separate summaryConfigs array with metric relationship @@ -78,9 +76,12 @@ export function buildEvalDataConfig(evaluators: EvaluatorSchema[]): { metricName: evaluator.metric.name, weight: weights[index], calculateType: CalculateMethodEnum.mean, + score: 0, summary: '', summaryStatus: SummaryStatusEnum.pending, - errorReason: '' + errorReason: '', + completedItemCount: 0, + overThresholdItemCount: 0 })); addLog.debug('[buildEvalDataConfig] Processed configuration:', { diff --git a/packages/service/core/evaluation/summary/worker.ts b/packages/service/core/evaluation/summary/worker.ts new file mode 100644 index 000000000000..814109bbd688 --- /dev/null +++ b/packages/service/core/evaluation/summary/worker.ts @@ -0,0 +1,181 @@ +import { getWorker, QueueNames } from '../../../common/bullmq'; +import { addLog } from '../../../common/system/log'; +import { MongoEvaluation } from '../task/schema'; +import { SummaryStatusEnum } from '@fastgpt/global/core/evaluation/constants'; +import type { EvaluationSchemaType } from '@fastgpt/global/core/evaluation/type'; +import { type EvaluationSummaryJobData } from './queue'; +import { EvaluationSummaryService } from './index'; +import { SummaryStatusHandler } from './statusHandler'; + +// 准备单个评估器任务 +async function prepareSingleEvaluatorTask( + evaluation: EvaluationSchemaType, + metricId: string +): Promise<{ + metricId: string; + evaluatorIndex: number; + evaluator: any; +} | null> { + const evaluatorIndex = evaluation.evaluators.findIndex( + (evaluator: any) => evaluator.metric._id.toString() === metricId + ); + + if (evaluatorIndex === -1) { + addLog.warn('[EvaluationSummary] Metric does not belong to this evaluation task', { + evalId: evaluation._id.toString(), + metricId + }); + return null; + } + + // Get metric name for logging + const metricName = evaluation.evaluators[evaluatorIndex].metric.name; + + return { + metricId, + evaluatorIndex, + evaluator: evaluation.evaluators[evaluatorIndex] + }; +} + +// 初始化评估总结Worker +export function initEvaluationSummaryWorker() { + const worker = getWorker( + QueueNames.evaluationSummary, + async (job) => { + const { evalId, metricId } = job.data; + + addLog.info('[EvaluationSummary] Worker processing single metric task', { + jobId: job.id, + evalId, + metricId + }); + + try { + // 获取评估任务数据 + const evaluation = await MongoEvaluation.findById(evalId).lean(); + if (!evaluation) { + throw new Error(`Evaluation task not found: ${evalId}`); + } + + // 验证和准备单个评估器任务 + const evaluatorTask = await prepareSingleEvaluatorTask(evaluation, metricId); + + if (!evaluatorTask) { + addLog.warn('[EvaluationSummary] No valid metric to process', { + evalId, + metricId + }); + return; + } + + // 状态更新将通过BullMQ事件监听器自动处理 + + // 执行单个指标的评估总结生成 + await EvaluationSummaryService.generateSingleMetricSummary( + evaluation, + evaluatorTask.metricId, + evaluatorTask.evaluatorIndex, + evaluatorTask.evaluator + ); + + addLog.info('[EvaluationSummary] Worker task completed successfully', { + jobId: job.id, + evalId, + metricId + }); + } catch (error) { + // 状态更新将通过BullMQ的failed事件监听器自动处理 + addLog.error('[EvaluationSummary] Worker task failed', { + jobId: job.id, + evalId, + metricId, + error + }); + throw error; + } + }, + { + concurrency: global.systemEnv?.evalConfig?.summaryConcurrency || 1, + removeOnComplete: { + count: 0 // 完成后立即删除,允许重复提交相同jobId + }, + removeOnFail: { + count: 0 // 失败后立即删除,允许重新提交失败的任务 + } + } + ); + + // 监听任务开始事件 + worker.on('active', async (job) => { + if (job?.data) { + const { evalId, metricId } = job.data; + + addLog.info('[EvaluationSummary] Task started', { + jobId: job.id, + evalId, + metricId, + timestamp: new Date().toISOString() + }); + + // 更新状态为generating + await SummaryStatusHandler.updateStatus( + evalId, + metricId, + SummaryStatusEnum.generating, + undefined, + new Date() + ); + } + }); + + // 监听任务完成事件 + worker.on('completed', async (job) => { + if (job?.data) { + const { evalId, metricId } = job.data; + + addLog.info('[EvaluationSummary] Task completed', { + jobId: job.id, + evalId, + metricId, + timestamp: new Date().toISOString() + }); + + // 更新状态为completed + await SummaryStatusHandler.updateStatus( + evalId, + metricId, + SummaryStatusEnum.completed, + undefined, + new Date() + ); + } + }); + + // 监听任务失败事件 + worker.on('failed', async (job, error) => { + if (job?.data) { + const { evalId, metricId } = job.data; + + addLog.warn('[EvaluationSummary] Task failed', { + jobId: job.id, + evalId, + metricId, + error: error.message, + timestamp: new Date().toISOString() + }); + + // 更新状态为failed + await SummaryStatusHandler.updateStatus( + evalId, + metricId, + SummaryStatusEnum.failed, + error.message, + new Date() + ); + } + }); + + addLog.info('[EvaluationSummary] Worker created successfully'); + return worker; +} diff --git a/packages/service/core/evaluation/target/index.ts b/packages/service/core/evaluation/target/index.ts index 113f0e1e1cc0..96e7bb9c3122 100644 --- a/packages/service/core/evaluation/target/index.ts +++ b/packages/service/core/evaluation/target/index.ts @@ -34,6 +34,7 @@ import { getUserChatInfoAndAuthTeamPoints } from '../../../support/permission/au import { getRunningUserInfoByTmbId } from '../../../support/user/team/utils'; import { removeDatasetCiteText } from '../../ai/utils'; import { saveChat } from '../../chat/saveChat'; +import { MongoChatItem } from '../../chat/chatItemSchema'; import { getChatTitleFromChatMessage } from '@fastgpt/global/core/chat/utils'; import { DispatchNodeResponseKeyEnum } from '@fastgpt/global/core/workflow/runtime/constants'; import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant'; @@ -81,8 +82,6 @@ export class WorkflowTarget extends EvaluationTarget { } async execute(input: TargetInput): Promise { - const startTime = Date.now(); - // Get application information const appData = await MongoApp.findById(this.config.appId); if (!appData) { @@ -112,40 +111,25 @@ export class WorkflowTarget extends EvaluationTarget { // Construct conversation history based on input.context const histories: (UserChatItemType | AIChatItemType)[] = []; + // Add context as background knowledge in conversation history if (input.context && input.context.length > 0) { - // Convert context strings to alternating user-ai conversation history - // Assume context format: [user1, ai1, user2, ai2, ...] - for (let i = 0; i < input.context.length; i++) { - const isUser = i % 2 === 0; - const content = input.context[i]; - - if (isUser) { - // User message - histories.push({ - obj: ChatRoleEnum.Human, - value: [ - { - type: ChatItemValueTypeEnum.text, - text: { - content - } - } - ] - }); - } else { - // AI message - histories.push({ - obj: ChatRoleEnum.AI, - value: [ - { - type: ChatItemValueTypeEnum.text, - text: { - content - } + const contextText = input.context + .filter((item) => item && item.trim()) + .map((item, index) => `${index + 1}. ${item}`) + .join('\n'); + + if (contextText) { + histories.push({ + obj: ChatRoleEnum.Human, + value: [ + { + type: ChatItemValueTypeEnum.text, + text: { + content: `请参考以下背景知识回答问题:\n${contextText}` } - ] - }); - } + } + ] + }); } } @@ -207,11 +191,27 @@ export class WorkflowTarget extends EvaluationTarget { durationSeconds }); + // Get the latest AI chat item dataId from the saved chat record + const latestAiChatItem = await MongoChatItem.findOne( + { + chatId, + appId: appData._id, + obj: ChatRoleEnum.AI + }, + 'dataId' + ) + .sort({ _id: -1 }) + .lean(); + + const aiChatItemDataId = latestAiChatItem?.dataId || ''; + return { actualOutput: response, retrievalContext: extractRetrievalContext(flowResponses), usage: flowUsages, - responseTime: Date.now() - startTime + responseTime: durationSeconds, + chatId, + aiChatItemDataId }; } diff --git a/packages/service/core/evaluation/task/errors.ts b/packages/service/core/evaluation/task/errors.ts new file mode 100644 index 000000000000..269149f8f8b2 --- /dev/null +++ b/packages/service/core/evaluation/task/errors.ts @@ -0,0 +1,151 @@ +import { UnrecoverableError } from 'bullmq'; +import { addLog } from '../../../common/system/log'; +import { TeamErrEnum } from '@fastgpt/global/common/error/code/team'; +import { EvaluationErrEnum } from '@fastgpt/global/common/error/code/evaluation'; +import { getErrText } from '@fastgpt/global/common/error/utils'; + +/** + * Unrecoverable evaluation error that prevents BullMQ auto-retry + */ +export class EvaluationUnrecoverableError extends UnrecoverableError { + constructor( + message: string, + public readonly stage: string + ) { + super(message); + this.name = 'EvaluationUnrecoverableError'; + } +} + +/** + * Retryable evaluation error that allows BullMQ auto-retry + */ +export class EvaluationRetryableError extends Error { + constructor( + message: string, + public readonly stage: string + ) { + super(message); + this.name = 'EvaluationRetryableError'; + } +} + +/** + * Error analysis result interface + */ +export interface ErrorAnalysisResult { + isRetriable: boolean; + category?: string; + pattern?: string; +} + +/** + * Simplified error analysis function for retry logic + */ +export const analyzeError = (error: any): ErrorAnalysisResult => { + const errorStr = error?.message || error?.code || String(error); + const lowerErrorStr = errorStr.toLowerCase(); + + // Check network-related errors + const networkErrors = [ + 'NETWORK_ERROR', + 'ECONNRESET', + 'ENOTFOUND', + 'ECONNREFUSED', + 'socket hang up', + 'timeout' + ]; + if (networkErrors.some((pattern) => lowerErrorStr.includes(pattern.toLowerCase()))) { + return { isRetriable: true, category: 'network' }; + } + + // Check HTTP status codes + const httpStatusMatch = errorStr.match(/\b(4\d{2}|5\d{2})\b/); + if (httpStatusMatch) { + const statusCode = httpStatusMatch[1]; + // 429 (Too Many Requests) and 5xx errors are retryable + if (statusCode === '429' || statusCode.startsWith('5')) { + return { + isRetriable: true, + category: statusCode.startsWith('5') ? 'serverError' : 'rateLimit' + }; + } + } + + return { isRetriable: false }; +}; + +/** + * Evaluation error context interface + */ +export interface EvaluationErrorContext { + evalId?: string; + evalItemId?: string; + resourceName?: string; +} + +/** + * Create appropriate BullMQ error type for auto-retry handling + */ +export const createEvaluationError = ( + error: any, + stage: string, + context?: EvaluationErrorContext, + forceRetry?: boolean +): Error => { + const errorStr = error?.message || error?.code || String(error); + + // Store error code instead of translated message for frontend translation + let errorMessage: string; + if ( + typeof error === 'string' && + Object.values(EvaluationErrEnum).includes(error as EvaluationErrEnum) + ) { + // Store the error code directly for frontend translation + errorMessage = error; + } else { + // For non-EvaluationErrEnum errors, still use getErrText for system errors + errorMessage = getErrText(error); + } + + // Build detailed error context + const logContext = { + stage, + error: errorStr, + originalError: error, + ...context + }; + + // Force retry for specific stages + if (forceRetry) { + addLog.warn(`[Evaluation] Force retryable error in stage ${stage}`, logContext); + return new EvaluationRetryableError(errorMessage, stage); + } + + // Unrecoverable error types + if ( + error === TeamErrEnum.aiPointsNotEnough || + error === EvaluationErrEnum.evalItemNotFound || + error === EvaluationErrEnum.evalTaskNotFound || + error == EvaluationErrEnum.evalDatasetLoadFailed || + error == EvaluationErrEnum.evalEvaluatorsConfigInvalid || + error == EvaluationErrEnum.evalTargetConfigInvalid + ) { + addLog.error(`[Evaluation] Unrecoverable error in stage ${stage}`, logContext); + return new EvaluationUnrecoverableError(errorMessage, stage); + } + + // Use existing error analysis logic to determine retry capability + const { isRetriable, category } = analyzeError(error); + + if (isRetriable) { + addLog.warn( + `[Evaluation] Retryable error in stage ${stage} (category: ${category})`, + logContext + ); + return new EvaluationRetryableError(errorMessage, stage); + } else { + addLog.error(`[Evaluation] Non-retryable error in stage ${stage}`, logContext); + return new EvaluationUnrecoverableError(errorMessage, stage); + } +}; diff --git a/packages/service/core/evaluation/task/index.ts b/packages/service/core/evaluation/task/index.ts index a4f6266b513b..e8c3014fcbab 100644 --- a/packages/service/core/evaluation/task/index.ts +++ b/packages/service/core/evaluation/task/index.ts @@ -1,23 +1,23 @@ import { MongoEvaluation, MongoEvalItem } from './schema'; +import { MongoEvalDatasetData } from '../dataset/evalDatasetDataSchema'; import type { EvaluationSchemaType, EvaluationItemSchemaType, CreateEvaluationParams, EvaluationItemDisplayType, TargetCallParams, - EvaluationDataItemType, EvaluationDisplayType } from '@fastgpt/global/core/evaluation/type'; -import type { DataItemListResponse } from '@fastgpt/global/core/evaluation/api'; -import type { MetricResult } from '@fastgpt/global/core/evaluation/metric/type'; import { Types } from 'mongoose'; import { EvaluationStatusEnum } from '@fastgpt/global/core/evaluation/constants'; import { - evaluationTaskQueue, - evaluationItemQueue, removeEvaluationTaskJob, removeEvaluationItemJobs, - removeEvaluationItemJobsByItemId + removeEvaluationItemJobsByItemId, + addEvaluationTaskJob, + addEvaluationItemJob, + checkEvaluationTaskJobActive, + evaluationItemQueue } from './mq'; import { createEvaluationUsage } from '../../../support/wallet/usage/controller'; import { addLog } from '../../../common/system/log'; @@ -25,11 +25,69 @@ import { buildEvalDataConfig } from '../summary/util/weightCalculator'; import { EvaluationErrEnum } from '@fastgpt/global/common/error/code/evaluation'; import { mongoSessionRun } from '../../../common/mongo/sessionRun'; import { type ClientSession } from '../../../common/mongo'; - -// Constants -const MAX_EXPORT_PAGE_SIZE = 100000; +import { + getEvaluationTaskStatus, + getEvaluationItemStatus, + getEvaluationTaskStats, + getBatchEvaluationItemStatus +} from './statusCalculator'; +import { EvaluationSummaryService } from '../summary'; export class EvaluationTaskService { + /** + * Build evaluator failure checks for MongoDB aggregation + */ + private static buildEvaluatorFailChecks(evaluators: any[]) { + return evaluators.map((evaluator, index) => { + const threshold = evaluator.thresholdValue || 0.8; + return { + $or: [ + { + $eq: [ + { + $let: { + vars: { + evaluatorOutput: { $arrayElemAt: ['$evaluatorOutputs', index] } + }, + in: '$$evaluatorOutput.data.score' + } + }, + null + ] + }, + { + $eq: [ + { + $type: { + $let: { + vars: { + evaluatorOutput: { $arrayElemAt: ['$evaluatorOutputs', index] } + }, + in: '$$evaluatorOutput.data.score' + } + } + }, + 'missing' + ] + }, + { + $lt: [ + { + $let: { + vars: { + evaluatorOutput: { $arrayElemAt: ['$evaluatorOutputs', index] } + }, + in: '$$evaluatorOutput.data.score' + } + }, + threshold + ] + } + ] + }; + }); + } + static async createEvaluation( params: CreateEvaluationParams & { teamId: string; @@ -38,19 +96,19 @@ export class EvaluationTaskService { ): Promise { const { teamId, tmbId, autoStart = true, ...evaluationParams } = params; - // Create usage record + // Create evaluation usage record const { billId } = await createEvaluationUsage({ teamId, tmbId, appName: evaluationParams.name }); - // Apply default configuration to evaluators (weights, thresholds, etc.) + // Apply default configuration to evaluators const { evaluators: evaluatorsWithDefaultConfig, summaryConfigs } = buildEvalDataConfig( - evaluationParams.evaluators - ); + evaluationParams.evaluators + ); const createAndStart = async (session: ClientSession) => { - // Create evaluation within transaction + // Create evaluation in transaction const evaluation = await MongoEvaluation.create( [ { @@ -60,7 +118,6 @@ export class EvaluationTaskService { teamId, tmbId, usageId: billId, - status: EvaluationStatusEnum.queuing, createTime: new Date() } ], @@ -69,22 +126,46 @@ export class EvaluationTaskService { const evaluationObject = evaluation[0].toObject(); - // Auto-start the evaluation if autoStart is true - if (autoStart) { - // Update status to evaluating within transaction - await MongoEvaluation.updateOne( - { _id: evaluationObject._id }, - { $set: { status: EvaluationStatusEnum.evaluating } }, - { session } - ); + // Load dataset and create evaluation items + const dataItems = await MongoEvalDatasetData.find({ + evalDatasetCollectionId: evaluationParams.evalDatasetCollectionId, + teamId + }) + .session(session) + .lean(); + + if (dataItems.length === 0) { + throw new Error(EvaluationErrEnum.evalDatasetLoadFailed); + } + + // Create evaluation items + const evalItems: Omit[] = []; + for (const dataItem of dataItems) { + const evaluationDataItem = { + _id: dataItem._id, + userInput: dataItem.userInput, + expectedOutput: dataItem.expectedOutput, + context: dataItem.context, + targetCallParams: undefined + }; + + evalItems.push({ + evalId: evaluationObject._id, + dataItem: evaluationDataItem + }); + } + + // Insert evaluation items in transaction + const insertedItems = await MongoEvalItem.insertMany(evalItems, { session }); + addLog.debug(`[Evaluation] Created ${insertedItems.length} evaluation items`); - // Queue operation within transaction - if it fails, transaction will rollback - await evaluationTaskQueue.add(`eval_task_${evaluationObject._id}`, { + // Auto-start evaluation if enabled + if (autoStart) { + // Add to task queue with deduplication + await addEvaluationTaskJob({ evalId: evaluationObject._id.toString() }); - // Update status in returned object - evaluationObject.status = EvaluationStatusEnum.evaluating; addLog.debug(`[Evaluation] Task created and auto-started: ${evaluationObject._id}`); } else { addLog.debug(`[Evaluation] Task created: ${evaluationObject._id}`); @@ -104,7 +185,14 @@ export class EvaluationTaskService { if (!evaluation) { throw new Error(EvaluationErrEnum.evalTaskNotFound); } - return evaluation; + + // Get real-time status from job queues + const status = await getEvaluationTaskStatus(evalId); + + return { + ...evaluation, + status + }; } static async updateEvaluation( @@ -123,7 +211,7 @@ export class EvaluationTaskService { static async deleteEvaluation(evalId: string, teamId: string): Promise { const del = async (session: ClientSession) => { - // Remove related tasks from queue to prevent further processing + // Remove tasks from queue to prevent further processing const [taskCleanupResult, itemCleanupResult] = await Promise.all([ removeEvaluationTaskJob(evalId, { forceCleanActiveJobs: true, @@ -143,7 +231,7 @@ export class EvaluationTaskService { itemCleanup: itemCleanupResult }); - // Delete all evaluation items for this evaluation task + // Delete all evaluation items await MongoEvalItem.deleteMany({ evalId: new Types.ObjectId(evalId) }, { session }); const result = await MongoEvaluation.deleteOne( @@ -173,22 +261,15 @@ export class EvaluationTaskService { tmbId?: string, isOwner: boolean = false, appName?: string, - appId?: string, - versionId?: string + appId?: string ): Promise<{ list: EvaluationDisplayType[]; total: number }> { - // Build basic filter and pagination + // Build 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 = offset; const limit = pageSize; const sort = { createTime: -1 as const }; - // If not owner, filter by accessible resources + // Filter by accessible resources if not owner let finalFilter = filter; if (!isOwner && accessibleIds) { finalFilter = { @@ -200,15 +281,15 @@ export class EvaluationTaskService { }; } - // Build aggregation pipeline with target filtering + // Build aggregation pipeline const aggregationPipeline = [ { $match: finalFilter }, { $lookup: { from: 'eval_dataset_collections', - localField: 'datasetId', + localField: 'evalDatasetCollectionId', foreignField: '_id', - as: 'dataset' + as: 'evalDatasetCollection' } }, { @@ -246,8 +327,8 @@ export class EvaluationTaskService { } ]; - // Add target filtering stage if any target filters are provided - if (appName || appId || versionId) { + // Add target filtering if provided + if (appName || appId) { const targetFilter: any = {}; if (appName) { @@ -258,69 +339,36 @@ export class EvaluationTaskService { targetFilter['target.config.appId'] = appId; } - if (versionId) { - targetFilter['target.config.versionId'] = versionId; - } - aggregationPipeline.push({ $match: targetFilter }); } + // Add search key filtering + if (searchKey) { + aggregationPipeline.push({ + $match: { + $or: [ + { name: { $regex: searchKey, $options: 'i' } }, + { description: { $regex: searchKey, $options: 'i' } }, + { 'target.config.versionId': { $regex: searchKey, $options: 'i' } }, + { 'target.config.versionName': { $regex: searchKey, $options: 'i' } } + ] + } + }); + } + const [evaluations, total] = await Promise.all([ MongoEvaluation.aggregate([ ...aggregationPipeline, - // Add real-time statistics lookup - { - $lookup: { - from: 'eval_items', - let: { evalId: '$_id' }, - pipeline: [ - { - $match: { - $expr: { $eq: ['$evalId', '$evalId'] } - } - }, - { - $group: { - _id: null, - totalItems: { $sum: 1 }, - completedItems: { - $sum: { $cond: [{ $eq: ['$status', EvaluationStatusEnum.completed] }, 1, 0] } - }, - errorItems: { - $sum: { $cond: [{ $eq: ['$status', EvaluationStatusEnum.error] }, 1, 0] } - } - } - } - ], - as: 'realTimeStats' - } - }, { $addFields: { - datasetName: { $arrayElemAt: ['$dataset.name', 0] }, + evalDatasetCollectionName: { $arrayElemAt: ['$evalDatasetCollection.name', 0] }, + evalDatasetCollectionId: '$evalDatasetCollectionId', metricNames: { $map: { input: '$evaluators', as: 'evaluator', in: '$$evaluator.metric.name' } - }, - // Use real-time statistics if available, otherwise fallback to stored statistics - statistics: { - $cond: { - if: { $gt: [{ $size: '$realTimeStats' }, 0] }, - then: { - $let: { - vars: { stats: { $arrayElemAt: ['$realTimeStats', 0] } }, - in: { - totalItems: '$$stats.totalItems', - completedItems: '$$stats.completedItems', - errorItems: '$$stats.errorItems' - } - } - }, - else: '$statistics' - } } } }, @@ -330,9 +378,9 @@ export class EvaluationTaskService { name: 1, createTime: 1, finishTime: 1, - status: 1, errorMessage: 1, - datasetName: 1, + evalDatasetCollectionName: 1, + evalDatasetCollectionId: 1, target: { type: '$target.type', config: { @@ -344,7 +392,9 @@ export class EvaluationTaskService { } }, metricNames: 1, - statistics: 1, + evaluators: 1, // Add evaluators field for real-time calculation + summaryConfigs: 1, + aggregateScore: 1, tmbId: 1 } }, @@ -352,15 +402,78 @@ export class EvaluationTaskService { { $skip: skip }, { $limit: limit } ]), - // Get total count using the same aggregation pipeline (without pagination) + // Get total count without pagination MongoEvaluation.aggregate([...aggregationPipeline, { $count: 'total' }]).then( (result) => result[0]?.total || 0 ) ]); - // Return raw data - permissions will be handled in API layer + // Get real-time status and statistics + const evaluationsWithStatus = await Promise.all( + evaluations.map(async (evaluation) => { + const [status, statistics] = await Promise.all([ + getEvaluationTaskStatus(evaluation._id.toString()), + getEvaluationTaskStats(evaluation._id.toString()) + ]); + return { + ...evaluation, + status, + statistics + }; + }) + ); + + // Return data (permissions handled in API layer) + // Calculate real-time scores for each evaluation + const evaluationsWithRealTimeScores = await Promise.all( + evaluationsWithStatus.map(async (evaluation) => { + try { + // Calculate real-time metric scores and aggregate score + const calculatedData = await EvaluationSummaryService.calculateMetricScores(evaluation); + + // Update summaryConfigs with real-time calculated values + const updatedSummaryConfigs = evaluation.summaryConfigs.map((summaryConfig: any) => { + const metricData = calculatedData.metricsData.find( + (m) => m.metricId === summaryConfig.metricId + ); + return { + ...summaryConfig, + score: metricData?.metricScore || 0, + completedItemCount: metricData?.totalCount || 0, + overThresholdItemCount: metricData?.aboveThresholdCount || 0 + }; + }); + + return { + ...evaluation, + summaryConfigs: updatedSummaryConfigs, + aggregateScore: calculatedData.aggregateScore + }; + } catch (error) { + addLog.error('[listEvaluations] Failed to calculate real-time scores', { + evalId: evaluation._id, + error + }); + // Return evaluation with default score values if calculation fails + const defaultSummaryConfigs = evaluation.summaryConfigs.map((summaryConfig: any) => ({ + ...summaryConfig, + score: 0, + completedItemCount: 0, + overThresholdItemCount: 0 + })); + + return { + ...evaluation, + summaryConfigs: defaultSummaryConfigs, + aggregateScore: 0 + }; + } + }) + ); + + // Return data with real-time scores - permissions will be handled in API layer return { - list: evaluations, + list: evaluationsWithRealTimeScores, total }; } @@ -368,6 +481,14 @@ export class EvaluationTaskService { static async getEvaluationDetail(evalId: string, teamId: string): Promise { const evaluationResult = await MongoEvaluation.aggregate([ { $match: { _id: new Types.ObjectId(evalId), teamId: new Types.ObjectId(teamId) } }, + { + $lookup: { + from: 'eval_dataset_collections', + localField: 'evalDatasetCollectionId', + foreignField: '_id', + as: 'evalDatasetCollection' + } + }, { $addFields: { 'target.config.appObjectId': { $toObjectId: '$target.config.appId' } @@ -394,55 +515,13 @@ export class EvaluationTaskService { as: 'appVersion' } }, - // Add real-time statistics lookup - { - $lookup: { - from: 'eval_items', - let: { evalId: '$_id' }, - pipeline: [ - { - $match: { - $expr: { $eq: ['$evalId', '$evalId'] } - } - }, - { - $group: { - _id: null, - totalItems: { $sum: 1 }, - completedItems: { - $sum: { $cond: [{ $eq: ['$status', EvaluationStatusEnum.completed] }, 1, 0] } - }, - errorItems: { - $sum: { $cond: [{ $eq: ['$status', EvaluationStatusEnum.error] }, 1, 0] } - } - } - } - ], - as: 'realTimeStats' - } - }, { $addFields: { 'target.config.appName': { $arrayElemAt: ['$app.name', 0] }, 'target.config.avatar': { $arrayElemAt: ['$app.avatar', 0] }, 'target.config.versionName': { $arrayElemAt: ['$appVersion.versionName', 0] }, - // Use real-time statistics if available, otherwise fallback to stored statistics - statistics: { - $cond: { - if: { $gt: [{ $size: '$realTimeStats' }, 0] }, - then: { - $let: { - vars: { stats: { $arrayElemAt: ['$realTimeStats', 0] } }, - in: { - totalItems: '$$stats.totalItems', - completedItems: '$$stats.completedItems', - errorItems: '$$stats.errorItems' - } - } - }, - else: '$statistics' - } - } + evalDatasetCollectionName: { $arrayElemAt: ['$evalDatasetCollection.name', 0] }, + evalDatasetCollectionId: '$evalDatasetCollectionId' } }, { @@ -452,7 +531,8 @@ export class EvaluationTaskService { tmbId: 1, name: 1, description: 1, - datasetId: 1, + evalDatasetCollectionId: 1, + evalDatasetCollectionName: 1, target: { type: '$target.type', config: { @@ -464,116 +544,78 @@ export class EvaluationTaskService { } }, evaluators: 1, + summaryConfigs: 1, usageId: 1, - status: 1, createTime: 1, finishTime: 1, - errorMessage: 1, - statistics: 1 + errorMessage: 1 } } ]); const evaluation = evaluationResult[0]; if (!evaluation) { - throw new Error('Evaluation not found'); + throw new Error(EvaluationErrEnum.evalTaskNotFound); } - return evaluation; - } - - static async listEvaluationItems( - evalId: string, - teamId: string, - offset: number = 0, - pageSize: number = 20 - ): Promise<{ items: EvaluationItemDisplayType[]; total: number }> { - const evaluation = await this.getEvaluation(evalId, teamId); - - const skip = offset; - const limit = pageSize; - - const [items, total] = await Promise.all([ - MongoEvalItem.find({ evalId: evaluation._id }) - .sort({ createTime: -1 }) - .skip(skip) - .limit(limit) - .lean() - .then((items) => - items.map((item) => ({ - ...item, - evalItemId: item._id.toString() - })) - ), - MongoEvalItem.countDocuments({ evalId: evaluation._id }) - ]); + const status = await getEvaluationTaskStatus(evalId); + const stats = await getEvaluationTaskStats(evalId); - return { items, total }; + return { + ...evaluation, + status, + statistics: stats + }; } static async startEvaluation(evalId: string, teamId: string): Promise { const evaluation = await this.getEvaluation(evalId, teamId); - // Check if task can be started/restarted + // Check if task can be started using job status + const isJobActive = await checkEvaluationTaskJobActive(evalId); + + if (isJobActive) { + throw new Error('Evaluation task is already running'); + } + + // Let BullMQ handle most scenarios const canStart = evaluation.status === EvaluationStatusEnum.queuing || - (evaluation.status === EvaluationStatusEnum.error && - evaluation.errorMessage === 'Manually stopped'); + evaluation.status === EvaluationStatusEnum.completed || + evaluation.status === EvaluationStatusEnum.error; if (!canStart) { throw new Error(EvaluationErrEnum.evalInvalidStateTransition); } - // Update status to processing and clear error message if restarting - const updateData: any = { status: EvaluationStatusEnum.evaluating }; - const unsetData: any = {}; - - if (evaluation.status === EvaluationStatusEnum.error) { - unsetData.errorMessage = 1; - unsetData.finishTime = 1; - } - - const updateQuery: any = { $set: updateData }; - if (Object.keys(unsetData).length > 0) { - updateQuery.$unset = unsetData; - } - - // Use transaction to ensure atomicity between status update and queue submission - const startEval = async (session: ClientSession) => { - // Update status within transaction - const result = await MongoEvaluation.updateOne( - { _id: new Types.ObjectId(evalId), teamId: new Types.ObjectId(teamId) }, - updateQuery, - { session } - ); - - if (result.matchedCount === 0) { - throw new Error(EvaluationErrEnum.evalTaskNotFound); - } - - // Queue operation within transaction - if it fails, transaction will rollback - await evaluationTaskQueue.add(`eval_task_${evalId}`, { - evalId: evalId - }); - }; - - await mongoSessionRun(startEval); + await addEvaluationTaskJob({ + evalId: evalId + }); - const action = evaluation.status === EvaluationStatusEnum.error ? 'restarted' : 'started'; + const action = + evaluation.status === EvaluationStatusEnum.error + ? 'restarted' + : evaluation.status === EvaluationStatusEnum.completed + ? 'restarted' + : 'started'; addLog.debug(`[Evaluation] Task ${action}: ${evalId}`); } static async stopEvaluation(evalId: string, teamId: string): Promise { const evaluation = await this.getEvaluation(evalId, teamId); + // Check if task is running using job status + const isJobActive = await checkEvaluationTaskJobActive(evalId); + if ( + !isJobActive && ![EvaluationStatusEnum.evaluating, EvaluationStatusEnum.queuing].includes(evaluation.status) ) { throw new Error(EvaluationErrEnum.evalOnlyRunningCanStop); } const stopEval = async (session: ClientSession) => { - // Remove related tasks from queue + // Remove tasks from queue const [taskCleanupResult, itemCleanupResult] = await Promise.all([ removeEvaluationTaskJob(evalId, { forceCleanActiveJobs: true, @@ -593,29 +635,26 @@ export class EvaluationTaskService { itemCleanup: itemCleanupResult }); - // Update status to error (manually stopped) + // Set error state for manual stop await MongoEvaluation.updateOne( { _id: new Types.ObjectId(evalId) }, { $set: { - status: EvaluationStatusEnum.error, finishTime: new Date(), - errorMessage: 'Manually stopped' + errorMessage: EvaluationErrEnum.evalManuallyStopped } }, { session } ); - // Stop all related evaluation items + // Mark evaluation items as manually stopped await MongoEvalItem.updateMany( { - evalId: new Types.ObjectId(evalId), - status: { $in: [EvaluationStatusEnum.queuing, EvaluationStatusEnum.evaluating] } + evalId: new Types.ObjectId(evalId) }, { $set: { - status: EvaluationStatusEnum.error, - errorMessage: 'Manually stopped', + errorMessage: EvaluationErrEnum.evalManuallyStopped, finishTime: new Date() } }, @@ -637,45 +676,217 @@ export class EvaluationTaskService { evaluating: number; queuing: number; error: number; + failed: number; }> { + const evaluation = await this.getEvaluation(evalId, teamId); // Validate access + + // Get real-time status from job queues + const basicStats = await getEvaluationTaskStats(evalId); + + // Calculate failed count using threshold checks + const evaluators = evaluation.evaluators || []; + let failedCount = 0; + + if (evaluators.length > 0) { + const evaluatorFailChecks = this.buildEvaluatorFailChecks(evaluators); + + // Count items that fail threshold checks + const failedResult = await MongoEvalItem.aggregate([ + { $match: { evalId: new Types.ObjectId(evalId) } }, + { + $addFields: { + hasFailedEvaluator: + evaluatorFailChecks.length > 0 ? { $or: evaluatorFailChecks } : false + } + }, + { + $match: { + hasFailedEvaluator: true, + finishTime: { $exists: true }, // Only completed items + errorMessage: { $exists: false } // Exclude errors + } + }, + { $count: 'failed' } + ]); + + failedCount = failedResult[0]?.failed || 0; + } + + return { + ...basicStats, + failed: failedCount + }; + } + + /** + * Evaluation Item Management + */ + + static async listEvaluationItems( + evalId: string, + teamId: string, + offset: number = 0, + pageSize: number = 20, + options: { + status?: EvaluationStatusEnum; + belowThreshold?: boolean; + userInput?: string; + expectedOutput?: string; + actualOutput?: string; + } = {} + ): Promise<{ items: EvaluationItemDisplayType[]; total: number }> { const evaluation = await this.getEvaluation(evalId, teamId); + const { status, belowThreshold, userInput, expectedOutput, actualOutput } = options; - const [statsResult] = await MongoEvalItem.aggregate([ - { $match: { evalId: evaluation._id } }, - { - $group: { - _id: null, - total: { $sum: 1 }, - completed: { - $sum: { $cond: [{ $eq: ['$status', EvaluationStatusEnum.completed] }, 1, 0] } - }, - evaluating: { - $sum: { $cond: [{ $eq: ['$status', EvaluationStatusEnum.evaluating] }, 1, 0] } - }, - queuing: { - $sum: { $cond: [{ $eq: ['$status', EvaluationStatusEnum.queuing] }, 1, 0] } - }, - error: { - $sum: { $cond: [{ $eq: ['$status', EvaluationStatusEnum.error] }, 1, 0] } + // Build aggregation pipeline + const pipeline = this.buildEvaluationItemsPipeline( + evaluation, + { status, belowThreshold, userInput, expectedOutput, actualOutput }, + offset, + pageSize + ); + + try { + const [dataResult, countResult] = await Promise.all([ + MongoEvalItem.aggregate(pipeline.dataPipeline), + MongoEvalItem.aggregate(pipeline.countPipeline) + ]); + + const total = countResult[0]?.total || 0; + const items = dataResult.map((item) => ({ + ...item, + _id: String(item._id), + // Add evaluator info + evaluators: evaluation.evaluators.map((evaluator, index) => ({ + metric: evaluator.metric, + thresholdValue: evaluator.thresholdValue, + //check mongo schema need to change or not,add this for Front-end calculate aggreatescore threshold + weight: evaluation.summaryConfigs[index]?.weight || 0 + })), + // Add summary configs + summaryConfigs: evaluation.summaryConfigs + })); + + return { items, total }; + } catch (error) { + console.error('Failed to list evaluation items', { + evalId, + options, + offset, + pageSize, + error + }); + throw new Error('Failed to list evaluation items'); + } + } + + /** + * Build aggregation pipeline for evaluation items listing + */ + private static buildEvaluationItemsPipeline( + evaluation: any, + filters: { + status?: EvaluationStatusEnum; + belowThreshold?: boolean; + userInput?: string; + expectedOutput?: string; + actualOutput?: string; + }, + offset: number, + pageSize: number + ) { + const { status, belowThreshold, userInput, expectedOutput, actualOutput } = filters; + + // Base match conditions + const matchConditions: Record = { + evalId: evaluation._id + }; + + // Use metadata.status for status filtering + if (status !== undefined) { + matchConditions['metadata.status'] = status; + } + + // Add text search conditions + const searchConditions: any[] = []; + if (userInput && typeof userInput === 'string' && userInput.trim().length > 0) { + searchConditions.push({ + 'dataItem.userInput': { $regex: new RegExp(userInput.trim(), 'i') } + }); + } + if (expectedOutput && typeof expectedOutput === 'string' && expectedOutput.trim().length > 0) { + searchConditions.push({ + 'dataItem.expectedOutput': { $regex: new RegExp(expectedOutput.trim(), 'i') } + }); + } + if (actualOutput && typeof actualOutput === 'string' && actualOutput.trim().length > 0) { + searchConditions.push({ + 'targetOutput.actualOutput': { $regex: new RegExp(actualOutput.trim(), 'i') } + }); + } + + if (searchConditions.length > 0) { + matchConditions.$and = searchConditions; + } + + // Build pipeline stages + const commonPipeline: any[] = [{ $match: matchConditions }]; + + // Add status field using metadata.status + commonPipeline.push({ + $addFields: { + status: '$metadata.status' + } + }); + + // Add threshold filter if specified + if (belowThreshold) { + const evaluators = evaluation.evaluators || []; + if (evaluators.length > 0) { + const evaluatorFailChecks = this.buildEvaluatorFailChecks(evaluators); + commonPipeline.push({ + $addFields: { + hasFailedEvaluator: + evaluatorFailChecks.length > 0 ? { $or: evaluatorFailChecks } : false + } + }); + commonPipeline.push({ + $match: { + hasFailedEvaluator: true, + 'metadata.status': EvaluationStatusEnum.completed, // Only completed items + evaluatorOutputs: { $exists: true, $ne: null, $not: { $size: 0 } } // Valid evaluator outputs } + }); + } + } + + // Data pipeline + const dataPipeline = [ + ...commonPipeline, + { $sort: { createTime: -1 } }, + { $skip: offset }, + { $limit: pageSize }, + { + $project: { + _id: 1, + evalId: 1, + dataItem: 1, + targetOutput: 1, + evaluatorOutputs: 1, + status: 1, + createTime: 1, + updateTime: 1, + errorMessage: 1 } } - ]); + ]; - // Return stats with defaults for empty results - const result = { - total: statsResult?.total || 0, - completed: statsResult?.completed || 0, - evaluating: statsResult?.evaluating || 0, - queuing: statsResult?.queuing || 0, - error: statsResult?.error || 0 - }; + // Count pipeline + const countPipeline = [...commonPipeline, { $count: 'total' }]; - return result; + return { dataPipeline, countPipeline }; } - // ========================= Evaluation Item Related APIs ========================= - static async getEvaluationItem( itemId: string, teamId: string @@ -688,12 +899,17 @@ export class EvaluationTaskService { await this.getEvaluation(item.evalId, teamId); - return item; + // Get real-time status + const status = await getEvaluationItemStatus(itemId); + + return { + ...item, + status + }; } /** - * Build MongoDB update object with dot notation for evaluation data item updates - * @private + * Build MongoDB update object for evaluation data items */ private static buildEvaluationDataItemUpdateObject(updates: { userInput?: string; @@ -721,7 +937,6 @@ export class EvaluationTaskService { /** * Update evaluation item with data item fields - * Unified method for API layers to update evaluation items */ static async updateEvaluationItem( itemId: string, @@ -733,9 +948,10 @@ export class EvaluationTaskService { }, teamId: string ): Promise { - await this.getEvaluationItem(itemId, teamId); + const item = await this.getEvaluationItem(itemId, teamId); + const evaluation = await this.getEvaluation(item.evalId, teamId); - // Build MongoDB update object with dot notation + // Build MongoDB update object const updateObj = this.buildEvaluationDataItemUpdateObject(updates); if (Object.keys(updateObj).length === 0) { return; @@ -749,12 +965,52 @@ export class EvaluationTaskService { if (result.matchedCount === 0) { throw new Error(EvaluationErrEnum.evalItemNotFound); } + + // Re-queue item if updated + if (result.modifiedCount > 0) { + // Get the updated item to determine the evalId + const updatedItem = await MongoEvalItem.findById(itemId, 'evalId'); + if (updatedItem) { + const cleanupResult = await removeEvaluationItemJobsByItemId(itemId, { + forceCleanActiveJobs: true, + retryAttempts: 3, + retryDelay: 200 + }); + + addLog.debug('Queue cleanup completed for evaluation item deletion', { + itemId, + cleanup: cleanupResult + }); + + // Reset results and re-queue + const evaluatorOutputs = evaluation.evaluators.map((evaluator) => ({ + metricName: evaluator.metric.name + })); + + await MongoEvalItem.updateOne( + { _id: new Types.ObjectId(itemId) }, + { + $set: { + targetOutput: {}, + evaluatorOutputs + } + } + ); + // Re-submit to evaluation queue + await addEvaluationItemJob({ + evalId: updatedItem.evalId.toString(), + evalItemId: itemId + }); + + addLog.debug(`[Evaluation] Item updated and re-queued for evaluation: ${itemId}`); + } + } } static async deleteEvaluationItem(itemId: string, teamId: string): Promise { await this.getEvaluationItem(itemId, teamId); - // Remove related jobs from queue before deleting the item + // Remove jobs from queue before deleting const cleanupResult = await removeEvaluationItemJobsByItemId(itemId, { forceCleanActiveJobs: true, retryAttempts: 3, @@ -778,252 +1034,79 @@ export class EvaluationTaskService { 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) { - throw new Error(EvaluationErrEnum.evalOnlyFailedCanRetry); - } + // Find the failed job for this item by searching through failed jobs + const failedJobs = await evaluationItemQueue.getJobs(['failed']); + const job = failedJobs.find((j) => j.data.evalItemId === itemId); - // Check if item is in error status or retryable status - if ( - item.status !== EvaluationStatusEnum.error && - item.status !== EvaluationStatusEnum.queuing - ) { - throw new Error(EvaluationErrEnum.evalItemNoErrorToRetry); + if (!job) { + throw new Error(EvaluationErrEnum.evalItemJobNotFound); } - // Remove existing jobs for this item to prevent duplicates - const cleanupResult = await removeEvaluationItemJobsByItemId(itemId, { - forceCleanActiveJobs: true, - retryAttempts: 3, - retryDelay: 200 - }); + // Retry the job directly (active event will clear error state automatically) + await job.retry(); - addLog.debug('Queue cleanup completed for evaluation item retry', { + addLog.debug('Evaluation item retried successfully', { itemId, - cleanup: cleanupResult + evalId: item.evalId, + teamId }); - - // Use transaction for atomic status update and queue submission - const retryItem = async (session: ClientSession) => { - // Update status within transaction - const result = await MongoEvalItem.updateOne( - { _id: new Types.ObjectId(itemId) }, - { - $set: { - status: EvaluationStatusEnum.queuing, - retry: Math.max(item.retry || 0, 1), // Ensure at least 1 retry chance - targetOutput: {}, - evaluatorOutput: {} - }, - $unset: { - finishTime: 1, - errorMessage: 1 - } - }, - { session } - ); - - if (result.matchedCount === 0) { - throw new Error(EvaluationErrEnum.evalItemNotFound); - } - - // Queue operation within transaction - if it fails, transaction will rollback - await evaluationItemQueue.add(`eval_item_retry_${itemId}`, { - evalId: item.evalId, - evalItemId: itemId - }); - }; - - await mongoSessionRun(retryItem); - - addLog.debug(`[Evaluation] Evaluation item reset to queuing status and resubmitted: ${itemId}`); } static async retryFailedItems(evalId: string, teamId: string): Promise { - const evaluation = await this.getEvaluation(evalId, teamId); + await this.getEvaluation(evalId, teamId); // Validate evalId and teamId - const retryItems = async (session: ClientSession): Promise => { - // Find items that need to be retried - const itemsToRetry = await MongoEvalItem.find( - { - evalId: evaluation._id, - status: EvaluationStatusEnum.error - }, - '_id', - { session } - ).lean(); - - if (itemsToRetry.length === 0) { - return 0; - } + // Get all failed jobs for this evaluation + const failedJobs = await evaluationItemQueue.getJobs(['failed']); + const evaluationFailedJobs = failedJobs.filter((job) => job.data.evalId === evalId); - // Clean up existing jobs for all items that will be retried to prevent duplicates - const itemIds = itemsToRetry.map((item) => item._id.toString()); - const cleanupPromises = itemIds.map((itemId) => - removeEvaluationItemJobsByItemId(itemId, { - forceCleanActiveJobs: true, - retryAttempts: 3, - retryDelay: 200 - }) - ); - - const cleanupResults = await Promise.allSettled(cleanupPromises); - const successfulCleanups = cleanupResults.filter((r) => r.status === 'fulfilled').length; - - addLog.debug('Queue cleanup completed for batch retry failed items', { - evalId, - totalItems: itemsToRetry.length, - successfulCleanups, - failedCleanups: cleanupResults.length - successfulCleanups - }); - - // Batch update status - await MongoEvalItem.updateMany( - { - _id: { $in: itemsToRetry.map((item) => item._id) } - }, - { - $set: { - status: EvaluationStatusEnum.queuing, - targetOutput: {}, - evaluatorOutput: {} - }, - $unset: { - finishTime: 1, - errorMessage: 1 - }, - $inc: { - retry: 1 - } - }, - { session } - ); + if (evaluationFailedJobs.length === 0) { + addLog.warn('No failed jobs found to retry for evaluation', { evalId }); + return 0; + } - // Batch resubmit to queue - const jobs = itemsToRetry.map((item, index) => ({ - name: `eval_item_batch_retry_${evalId}_${index}`, - data: { - evalId: evaluation._id, - evalItemId: item._id.toString() - }, - opts: { - delay: index * 100 // Add small delay to avoid starting too many tasks simultaneously - } - })); + let retriedItems = 0; + let failedRetries = 0; + // Process each failed job + for (const job of evaluationFailedJobs) { try { - await evaluationItemQueue.addBulk(jobs); - } catch (queueError) { - // If queue operation fails, the transaction will rollback the status updates - addLog.error(`[Evaluation] Failed to resubmit jobs to queue: ${evalId}`, queueError); - throw queueError; + // Retry the job directly (active event will clear error state automatically) + await job.retry(); + retriedItems++; + } catch (error) { + failedRetries++; + addLog.error('Failed to retry individual evaluation item job', { + jobId: job.id, + evalId, + evalItemId: job.data.evalItemId, + teamId, + error + }); } + } - addLog.debug( - `[Evaluation] Batch retry failed items: ${evalId}, affected count: ${itemsToRetry.length}` - ); - - return itemsToRetry.length; - }; - - const retriedCount = await mongoSessionRun(retryItems); + addLog.debug('All failed evaluation items retry completed', { + evalId, + teamId, + totalFailedJobs: evaluationFailedJobs.length, + retriedItems, + failedRetries + }); - return retriedCount; + return retriedItems; } static async getEvaluationItemResult( itemId: string, teamId: string - ): Promise<{ - item: EvaluationItemSchemaType; - dataItem: EvaluationDataItemType; - response?: string; - result?: MetricResult; - score?: number; - }> { + ): Promise { const item = await this.getEvaluationItem(itemId, teamId); - - return { - item, - dataItem: item.dataItem, - response: item.targetOutput?.actualOutput, - result: item.evaluatorOutput, - score: item.evaluatorOutput?.data?.score - }; - } - - // Search evaluation items - static async searchEvaluationItems( - evalId: string, - teamId: string, - options: { - status?: EvaluationStatusEnum; - hasError?: boolean; - scoreRange?: { min?: number; max?: number }; - keyword?: string; - page?: number; - pageSize?: number; - } = {} - ): Promise<{ items: EvaluationItemDisplayType[]; total: number }> { - const evaluation = await this.getEvaluation(evalId, teamId); - - const { status, hasError, scoreRange, keyword, page = 1, pageSize = 20 } = options; - - // Build query conditions - const filter: any = { evalId: evaluation._id }; - - if (status !== undefined) { - filter.status = status; - } - - if (hasError === true) { - filter.status = EvaluationStatusEnum.error; - } else if (hasError === false) { - filter.status = { $ne: EvaluationStatusEnum.error }; - } - - if (scoreRange) { - const scoreFilter: any = {}; - if (scoreRange.min !== undefined) { - scoreFilter.$gte = scoreRange.min; - } - if (scoreRange.max !== undefined) { - scoreFilter.$lte = scoreRange.max; - } - if (Object.keys(scoreFilter).length > 0) { - filter['evaluatorOutput.data.score'] = scoreFilter; - } - } - - if (keyword) { - filter.$or = [ - { 'dataItem.userInput': { $regex: keyword, $options: 'i' } }, - { 'dataItem.expectedOutput': { $regex: keyword, $options: 'i' } }, - { 'targetOutput.actualOutput': { $regex: keyword, $options: 'i' } } - ]; - } - - const skip = (page - 1) * pageSize; - - const [items, total] = await Promise.all([ - MongoEvalItem.find(filter) - .sort({ createTime: -1 }) - .skip(skip) - .limit(pageSize) - .lean() - .then((items) => - items.map((item) => ({ - ...item, - evalItemId: item._id.toString() - })) - ), - MongoEvalItem.countDocuments(filter) - ]); - - return { items, total }; + return item; } - // Export evaluation item results + /** + * Export evaluation results + */ static async exportEvaluationResults( evalId: string, teamId: string, @@ -1037,16 +1120,20 @@ export class EvaluationTaskService { const total = items.length; + // Get real-time status for all items + const itemIds = items.map((item) => item._id.toString()); + const statusMap = await getBatchEvaluationItemStatus(itemIds); + if (format === 'json') { const results = items.map((item) => ({ itemId: item._id, userInput: item.dataItem?.userInput, expectedOutput: item.dataItem?.expectedOutput, actualOutput: item.targetOutput?.actualOutput, - score: item.evaluatorOutput?.data?.score, - status: item.status, + scores: item.evaluatorOutputs?.map((output) => output?.data?.score) || [], + status: statusMap.get(item._id.toString()) || EvaluationStatusEnum.completed, targetOutput: item.targetOutput, - evaluatorOutput: item.evaluatorOutput, + evaluatorOutputs: item.evaluatorOutputs, errorMessage: item.errorMessage, finishTime: item.finishTime })); @@ -1058,12 +1145,23 @@ export class EvaluationTaskService { return { results: Buffer.from(''), total: 0 }; } + // Collect all unique metric names from evaluator outputs + const metricNames = new Set(); + items.forEach((item) => { + item.evaluatorOutputs?.forEach((output) => { + if (output?.data?.metricName) { + metricNames.add(output.data.metricName); + } + }); + }); + const sortedMetricNames = Array.from(metricNames).sort(); + const headers = [ 'ItemId', 'UserInput', 'ExpectedOutput', 'ActualOutput', - 'Score', + ...sortedMetricNames, 'Status', 'ErrorMessage', 'FinishTime' @@ -1072,13 +1170,27 @@ export class EvaluationTaskService { const csvRows = [headers.join(',')]; items.forEach((item) => { + // Create a map of metric name to score for easier lookup + const metricScoreMap = new Map(); + item.evaluatorOutputs?.forEach((output) => { + if (output?.data?.metricName && output.data.score !== undefined) { + metricScoreMap.set(output.data.metricName, output.data.score); + } + }); + + const itemStatus = statusMap.get(item._id.toString()) || EvaluationStatusEnum.completed; + const row = [ item._id.toString(), `"${(item.dataItem?.userInput || '').replace(/"/g, '""')}"`, `"${(item.dataItem?.expectedOutput || '').replace(/"/g, '""')}"`, `"${(item.targetOutput?.actualOutput || '').replace(/"/g, '""')}"`, - item.evaluatorOutput?.data?.score || '', - item.status || '', + // Add scores for each metric column in the same order as headers + ...sortedMetricNames.map((metricName) => { + const score = metricScoreMap.get(metricName); + return score !== undefined ? score : ''; + }), + itemStatus || '', `"${(item.errorMessage || '').replace(/"/g, '""')}"`, item.finishTime || '' ]; @@ -1088,351 +1200,5 @@ export class EvaluationTaskService { return { results: Buffer.from(csvRows.join('\n')), total }; } } - - // ========================= DataItem Aggregation APIs ========================= - - static async listDataItemsGrouped( - teamId: string, - options: { - evalId: string; - status?: number; - keyword?: string; - offset?: number; - pageSize?: number; - } - ): Promise { - const { evalId, status, keyword, offset = 0, pageSize = 20 } = options; - - // Verify team access to the evaluation task - await this.getEvaluation(evalId, teamId); - - // Build match stage - const matchStage: any = { - evalId: new Types.ObjectId(evalId) - }; - - if (status !== undefined) { - matchStage.status = status; - } - - if (keyword) { - matchStage.$or = [ - { 'dataItem.userInput': { $regex: keyword, $options: 'i' } }, - { 'dataItem.expectedOutput': { $regex: keyword, $options: 'i' } } - ]; - } - - // Build aggregation pipeline - const aggregationPipeline = [ - { $match: matchStage }, - { - $group: { - _id: '$dataItem._id', - dataItem: { $first: '$dataItem' }, - items: { $push: '$$ROOT' }, - totalItems: { $sum: 1 }, - completedItems: { - $sum: { $cond: [{ $eq: ['$status', EvaluationStatusEnum.completed] }, 1, 0] } - }, - errorItems: { - $sum: { $cond: [{ $eq: ['$status', EvaluationStatusEnum.error] }, 1, 0] } - } - } - }, - { - $addFields: { - dataItemId: '$_id', - 'statistics.totalItems': '$totalItems', - 'statistics.completedItems': '$completedItems', - 'statistics.errorItems': '$errorItems' - } - }, - { $sort: { totalItems: -1 as const, _id: 1 as const } } - ]; - - // Simple Promise.all approach like listEvaluationItems - const [list, total] = await Promise.all([ - // Get paginated results with projection - MongoEvalItem.aggregate([ - ...aggregationPipeline, - { $skip: offset }, - { $limit: pageSize }, - { - $project: { - dataItemId: 1, - dataItem: 1, - items: { - $map: { - input: '$items', - as: 'item', - in: { - $mergeObjects: ['$$item', { evalItemId: { $toString: '$$item._id' } }] - } - } - }, - statistics: { - totalItems: '$totalItems', - completedItems: '$completedItems', - errorItems: '$errorItems' - } - } - } - ]), - // Get total count - MongoEvalItem.aggregate([...aggregationPipeline, { $count: 'total' }]).then( - (result) => result[0]?.total || 0 - ) - ]); - - return { - list, - total - }; - } - - static async deleteEvaluationItemsByDataItem( - dataItemId: string, - teamId: string, - evalId: string - ): Promise<{ deletedCount: number }> { - // Verify team access to the evaluation task - await this.getEvaluation(evalId, teamId); - - const filter: any = { - 'dataItem._id': new Types.ObjectId(dataItemId), - evalId: new Types.ObjectId(evalId) - }; - - // Find items to delete - const itemsToDelete = await MongoEvalItem.find(filter).lean(); - - if (itemsToDelete.length === 0) { - return { deletedCount: 0 }; - } - - const deleteOperation = async (session: ClientSession) => { - // Clean up queue jobs for items to be deleted - const itemIds = itemsToDelete.map((item) => item._id.toString()); - const cleanupPromises = itemIds.map((itemId) => - removeEvaluationItemJobsByItemId(itemId, { - forceCleanActiveJobs: true, - retryAttempts: 3, - retryDelay: 200 - }) - ); - - await Promise.allSettled(cleanupPromises); - - // Delete the items - const result = await MongoEvalItem.deleteMany(filter, { session }); - - addLog.debug(`[Evaluation] Deleted ${result.deletedCount} items for dataItem: ${dataItemId}`); - - return result.deletedCount; - }; - - const deletedCount = await mongoSessionRun(deleteOperation); - - return { - deletedCount - }; - } - - static async retryEvaluationItemsByDataItem( - dataItemId: string, - teamId: string, - evalId: string - ): Promise<{ retriedCount: number }> { - // Verify evaluation access first - await this.getEvaluation(evalId, teamId); - - const filter: any = { - 'dataItem._id': new Types.ObjectId(dataItemId), - evalId: new Types.ObjectId(evalId), - status: EvaluationStatusEnum.error - }; - - // Find items to retry - const itemsToRetry = await MongoEvalItem.find(filter).lean(); - - if (itemsToRetry.length === 0) { - return { retriedCount: 0 }; - } - - const retryOperation = async (session: ClientSession) => { - // Clean up existing jobs - const itemIds = itemsToRetry.map((item) => item._id.toString()); - const cleanupPromises = itemIds.map((itemId) => - removeEvaluationItemJobsByItemId(itemId, { - forceCleanActiveJobs: true, - retryAttempts: 3, - retryDelay: 200 - }) - ); - - await Promise.allSettled(cleanupPromises); - - // Update items status - const result = await MongoEvalItem.updateMany( - { _id: { $in: itemsToRetry.map((item) => item._id) } }, - { - $set: { - status: EvaluationStatusEnum.queuing, - targetOutput: {}, - evaluatorOutput: {} - }, - $unset: { - finishTime: 1, - errorMessage: 1 - }, - $inc: { retry: 1 } - }, - { session } - ); - - // Resubmit to queue - const jobs = itemsToRetry.map((item, index) => ({ - name: `eval_item_dataitem_retry_${dataItemId}_${index}`, - data: { - evalId: item.evalId, - evalItemId: item._id.toString() - }, - opts: { - delay: index * 100 - } - })); - - await evaluationItemQueue.addBulk(jobs); - - addLog.debug( - `[Evaluation] Retried ${result.modifiedCount} items for dataItem: ${dataItemId}` - ); - - return result.modifiedCount; - }; - - const retriedCount = await mongoSessionRun(retryOperation); - - return { - retriedCount - }; - } - - static async updateEvaluationItemsByDataItem( - dataItemId: string, - updates: { - userInput?: string; - expectedOutput?: string; - context?: string[]; - targetCallParams?: TargetCallParams; - }, - teamId: string, - evalId: string - ): Promise<{ updatedCount: number }> { - // Verify evaluation access first - await this.getEvaluation(evalId, teamId); - - // Build MongoDB update object with dot notation - const updateObj = this.buildEvaluationDataItemUpdateObject(updates); - if (Object.keys(updateObj).length === 0) { - return { updatedCount: 0 }; - } - - const filter: any = { - 'dataItem._id': new Types.ObjectId(dataItemId), - evalId: new Types.ObjectId(evalId) - }; - - const result = await MongoEvalItem.updateMany(filter, { $set: updateObj }); - - addLog.debug(`[Evaluation] Updated ${result.modifiedCount} items for dataItem: ${dataItemId}`); - - return { - updatedCount: result.modifiedCount - }; - } - - static async exportEvaluationResultsGroupedByDataItem( - teamId: string, - evalId: string, - format: 'csv' | 'json' = 'json' - ): Promise<{ results: Buffer; totalItems: number }> { - // Get evaluation config for metric names - const evaluation = await this.getEvaluation(evalId, teamId); - - // Use listDataItemsGrouped to get all dataItems (large pageSize to get all) - const { list: dataItems } = await this.listDataItemsGrouped(teamId, { - evalId, - offset: 0, - pageSize: MAX_EXPORT_PAGE_SIZE // Large pageSize to get all items - }); - - if (dataItems.length === 0) { - const emptyResult = format === 'json' ? '[]' : ''; - return { - results: Buffer.from(emptyResult), - totalItems: 0 - }; - } - - // Extract metric names from evaluation config - const metricNames = evaluation.evaluators.map( - (evaluator) => evaluator.metric.name || evaluator.metric._id || 'Unknown Metric' - ); - - // Transform listDataItemsGrouped result to export format (remove totalItems, completedItems, errorItems) - const exportData = dataItems.map((groupedItem: any) => { - const dataItemExport = { - dataItemId: groupedItem.dataItemId, - userInput: groupedItem.dataItem?.userInput, - expectedOutput: groupedItem.dataItem?.expectedOutput, - actualOutput: groupedItem.items.find((item: any) => item.targetOutput?.actualOutput) - ?.targetOutput?.actualOutput, - // Build metric scores object - metricScores: {} as Record - }; - - // Add scores for each metric from the grouped items - groupedItem.items.forEach((item: any) => { - if (item.evaluator?.metric?.name && item.evaluatorOutput?.data?.score !== undefined) { - const metricName = item.evaluator.metric.name; - dataItemExport.metricScores[metricName] = item.evaluatorOutput.data.score; - } - }); - - return dataItemExport; - }); - - if (format === 'json') { - return { - results: Buffer.from(JSON.stringify(exportData, null, 2)), - totalItems: exportData.length - }; - } else { - // CSV format with dynamic metric columns (remove totalItems, completedItems, errorItems) - const baseHeaders = ['DataItemId', 'UserInput', 'ExpectedOutput', 'ActualOutput']; - - const headers = [...baseHeaders, ...metricNames]; - const csvRows = [headers.join(',')]; - - exportData.forEach((dataItem) => { - const row = [ - dataItem.dataItemId, - `"${(dataItem.userInput || '').replace(/"/g, '""')}"`, - `"${(dataItem.expectedOutput || '').replace(/"/g, '""')}"`, - `"${(dataItem.actualOutput || '').replace(/"/g, '""')}"`, - // Add metric scores in the same order as headers - ...metricNames.map((metricName) => dataItem.metricScores[metricName] || '') - ]; - - csvRows.push(row.join(',')); - }); - - return { - results: Buffer.from(csvRows.join('\n')), - totalItems: exportData.length - }; - } - } } export { MongoEvaluation }; diff --git a/packages/service/core/evaluation/task/mq.ts b/packages/service/core/evaluation/task/mq.ts index 1d20bccaecd5..2228755a953c 100644 --- a/packages/service/core/evaluation/task/mq.ts +++ b/packages/service/core/evaluation/task/mq.ts @@ -4,38 +4,237 @@ import type { EvaluationTaskJobData, EvaluationItemJobData } from '@fastgpt/global/core/evaluation/type'; +import { EvaluationStatusEnum } from '@fastgpt/global/core/evaluation/constants'; import { createJobCleaner, type JobCleanupResult, type JobCleanupOptions } from '../utils/jobCleanup'; +import { MongoEvaluation } from './schema'; +import { getErrText } from '@fastgpt/global/common/error/utils'; export const evaluationTaskQueue = getQueue(QueueNames.evalTask, { defaultJobOptions: { - attempts: 1, // The task queue does not retry, and errors are handled internally - removeOnComplete: 100, - removeOnFail: 100 + attempts: 3, // Enable retry for task level + backoff: { + type: 'exponential', + delay: 2000 + }, + removeOnComplete: false, + removeOnFail: false } }); export const evaluationItemQueue = getQueue(QueueNames.evalTaskItem, { defaultJobOptions: { - attempts: 1, // Disable BullMQ retry, use manual retry mechanism instead - removeOnComplete: 500, - removeOnFail: 500 + attempts: (global.systemEnv?.evalConfig?.caseMaxRetry || 3) + 1, // Enable retry: max 4 attempts (1 initial + 3 retries) + backoff: { + type: 'exponential', + delay: 1000 // Initial delay 1s, exponential backoff + }, + removeOnComplete: false, + removeOnFail: false } }); -export const getEvaluationTaskWorker = (processor: any) => - getWorker(QueueNames.evalTask, processor, { - concurrency: Number(process.env.EVAL_TASK_CONCURRENCY) || 3 +export const getEvaluationTaskWorker = (processor: any) => { + const worker = getWorker(QueueNames.evalTask, processor, { + concurrency: global.systemEnv?.evalConfig?.taskConcurrency || 3, + stalledInterval: 30000, // 30 seconds + maxStalledCount: global.systemEnv?.evalConfig?.maxStalledCount || 3 }); -export const getEvaluationItemWorker = (processor: any) => - getWorker(QueueNames.evalTaskItem, processor, { - concurrency: Number(process.env.EVAL_ITEM_CONCURRENCY) || 10 + worker.on('stalled', async (jobId: string) => { + const job = await evaluationTaskQueue.getJob(jobId); + addLog.warn('[Evaluation] Task job stalled, will be retried', { + jobId, + evalId: job?.data?.evalId + }); }); + worker.on('failed', async (job, error) => { + try { + const evalId = job?.data.evalId; + addLog.error('[Evaluation] Task job failed after all retries', { + jobId: job?.id, + evalId, + error + }); + await MongoEvaluation.updateOne( + { _id: evalId }, + { + $set: { + errorMessage: getErrText(error), + finishTime: new Date() + } + } + ); + } catch (updateError) { + addLog.error('[Evaluation] Task job failed after all retries (could not get job data)', { + jobId: job?.id, + error, + updateError + }); + } + }); + + return worker; +}; + +export const getEvaluationItemWorker = (processor: any) => { + const worker = getWorker(QueueNames.evalTaskItem, processor, { + concurrency: global.systemEnv?.evalConfig?.caseConcurrency || 10, + stalledInterval: 30000, // 30 seconds for faster recovery + maxStalledCount: global.systemEnv?.evalConfig?.maxStalledCount || 3 + }); + worker.on('stalled', async (jobId) => { + try { + const job = await evaluationItemQueue.getJob(jobId); + const evalItemId = job?.data?.evalItemId; + + addLog.warn('[Evaluation] Item job stalled, will be retried', { + jobId, + evalId: job?.data?.evalId, + evalItemId + }); + + // Update status to queuing since stalled jobs will be retried + if (evalItemId) { + const { MongoEvalItem } = await import('./schema'); + await MongoEvalItem.updateOne( + { _id: evalItemId }, + { + $set: { + 'metadata.status': EvaluationStatusEnum.queuing + }, + $unset: { + finishTime: 1, + errorMessage: 1 + } + } + ); + } + } catch (error) { + addLog.warn('[Evaluation] Item job stalled, will be retried (could not get job data)', { + jobId, + error + }); + } + }); + worker.on('failed', async (job, error) => { + // Handle failed items and check task completion + + try { + const evalId = job?.data?.evalId; + const evalItemId = job?.data?.evalItemId; + // Update item status to error + if (evalItemId) { + const { MongoEvalItem } = await import('./schema'); + await MongoEvalItem.updateOne( + { _id: evalItemId }, + { + $set: { + errorMessage: getErrText(error), + finishTime: new Date(), + 'metadata.status': EvaluationStatusEnum.error + } + } + ); + } + // Check task completion after failure + if (evalId) { + addLog.debug('[Evaluation] Checking task completion after item failure', { + jobId: job?.id, + evalId, + evalItemId, + error + }); + + // Check task completion (avoid circular dependency) + const { finishEvaluationTask } = await import('./processor'); + await finishEvaluationTask(evalId); + } + } catch (finishError) { + addLog.warn('[Evaluation] Could not retrieve job data for failed job', { + jobId: job?.id, + finishError + }); + } + }); + + worker.on('active', async (job) => { + try { + const evalId = job?.data?.evalId; + const evalItemId = job?.data?.evalItemId; + if (evalItemId) { + addLog.debug('[Evaluation] Item job started, updating status to evaluating', { + jobId: job?.id, + evalId, + evalItemId + }); + + // Update item status to evaluating + const { MongoEvalItem } = await import('./schema'); + await MongoEvalItem.updateOne( + { _id: evalItemId }, + { + $set: { + 'metadata.status': EvaluationStatusEnum.evaluating + }, + $unset: { + finishTime: 1, + errorMessage: 1 + } + } + ); + } + } catch (error) { + addLog.error('[Evaluation] Error in active event handler', { + jobId: job?.id, + error + }); + } + }); + + worker.on('completed', async (job) => { + try { + const evalId = job?.data?.evalId; + const evalItemId = job?.data?.evalItemId; + if (evalId && evalItemId) { + addLog.debug( + '[Evaluation] Item completed, updating metadata and checking task completion', + { + jobId: job?.id, + evalId, + evalItemId + } + ); + + // Update item status to completed + const { MongoEvalItem } = await import('./schema'); + await MongoEvalItem.updateOne( + { _id: evalItemId }, + { + $set: { + 'metadata.status': EvaluationStatusEnum.completed, + finishTime: new Date() + } + } + ); + + // Check task completion (avoid circular dependency) + const { finishEvaluationTask } = await import('./processor'); + await finishEvaluationTask(job.data.evalId); + } + } catch (error) { + addLog.error('[Evaluation] Error in completed event handler', { + jobId: job?.id, + error + }); + } + }); +}; + export const removeEvaluationTaskJob = async ( evalId: string, options?: JobCleanupOptions @@ -108,14 +307,77 @@ export const removeEvaluationItemJobsByItemId = async ( return result; }; -export const getEvaluationQueueStats = async () => { - const [taskStats, itemStats] = await Promise.all([ - evaluationTaskQueue.getJobCounts('active', 'prioritized', 'waiting', 'completed', 'failed'), - evaluationItemQueue.getJobCounts('active', 'prioritized', 'waiting', 'completed', 'failed') - ]); +export const addEvaluationTaskJob = (data: EvaluationTaskJobData) => { + const evalId = String(data.evalId); - return { - taskQueue: taskStats, - itemQueue: itemStats - }; + return evaluationTaskQueue.add(evalId, data, { deduplication: { id: evalId, ttl: 5000 } }); +}; + +export const addEvaluationItemJob = (data: EvaluationItemJobData, options?: { delay?: number }) => { + const evalItemId = String(data.evalItemId); + + return evaluationItemQueue.add(evalItemId, data, { + deduplication: { + id: evalItemId, + ttl: 5000 + }, + ...options + }); +}; + +export const addEvaluationItemJobs = ( + jobs: Array<{ + data: EvaluationItemJobData; + delay?: number; + }> +) => { + const bulkJobs = jobs.map(({ data, delay }, index) => { + const evalItemId = String(data.evalItemId); + return { + name: evalItemId, + data, + opts: { + delay: delay ?? index * 100, // Small delay to avoid overwhelming system + deduplication: { id: evalItemId } + } + }; + }); + + return evaluationItemQueue.addBulk(bulkJobs); +}; + +export const checkEvaluationTaskJobActive = async (evalId: string): Promise => { + try { + // Check active jobs first (most likely state for active tasks) + const activeJobs = await evaluationTaskQueue.getJobs([ + 'active', + 'waiting', + 'delayed', + 'prioritized' + ]); + const job = activeJobs.find((j) => j.data.evalId === evalId); + + return job !== undefined; + } catch (error) { + addLog.error('[Evaluation] Failed to check task job status', { evalId, error }); + return false; + } +}; + +export const checkEvaluationItemJobActive = async (evalItemId: string): Promise => { + try { + // Check active jobs first (most likely state for active items) + const activeJobs = await evaluationItemQueue.getJobs([ + 'active', + 'waiting', + 'delayed', + 'prioritized' + ]); + const job = activeJobs.find((j) => j.data.evalItemId === evalItemId); + + return job !== undefined; + } catch (error) { + addLog.error('[Evaluation] Failed to check item job status', { evalItemId, error }); + return false; + } }; diff --git a/packages/service/core/evaluation/task/processor.ts b/packages/service/core/evaluation/task/processor.ts index 2ff97840bfde..287286e74067 100644 --- a/packages/service/core/evaluation/task/processor.ts +++ b/packages/service/core/evaluation/task/processor.ts @@ -2,729 +2,414 @@ import { addLog } from '../../../common/system/log'; import type { Job } from '../../../common/bullmq'; import type { EvaluationTaskJobData, - EvaluationItemJobData + EvaluationItemJobData, + TargetOutput } from '@fastgpt/global/core/evaluation/type'; -import { evaluationItemQueue, getEvaluationItemWorker, getEvaluationTaskWorker } from './mq'; +import { getEvaluationItemWorker, getEvaluationTaskWorker, addEvaluationItemJobs } from './mq'; import { MongoEvaluation, MongoEvalItem } from './schema'; -import { MongoEvalDatasetData } from '../dataset/evalDatasetDataSchema'; import { createTargetInstance } from '../target'; import { createEvaluatorInstance } from '../evaluator'; import { Types } from 'mongoose'; import { EvaluationStatusEnum } from '@fastgpt/global/core/evaluation/constants'; import { checkTeamAIPoints } from '../../../support/permission/teamLimit'; -import { TeamErrEnum } from '@fastgpt/global/common/error/code/team'; import { EvaluationErrEnum } from '@fastgpt/global/common/error/code/evaluation'; import { getErrText } from '@fastgpt/global/common/error/utils'; import { createMergedEvaluationUsage } from '../utils/usage'; import { EvaluationSummaryService } from '../summary'; +import { getBatchEvaluationItemStatus } from './statusCalculator'; +import { createEvaluationError } from './errors'; -// Sleep utility function -const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); - -// Evaluation stage error types -export enum EvaluationStageEnum { - TaskExecute = 'TaskExecute', - EvaluatorExecute = 'EvaluatorExecute', - ResourceCheck = 'ResourceCheck' -} - -// Structured error class for evaluation stages -export class EvaluationStageError extends Error { - public readonly stage: EvaluationStageEnum; - public readonly originalError: any; - public readonly retriable: boolean; - - constructor( - stage: EvaluationStageEnum, - errorMsg: string, - retriable: boolean, - originalError?: any - ) { - super(errorMsg); - this.name = 'EvaluationStageError'; - this.stage = stage; - this.originalError = originalError; - this.retriable = retriable; - } - - toString(): string { - return `[${this.stage}] ${this.message}`; - } -} - -// Distributed lock implementation -const distributedLocks = new Map(); - -const acquireDistributedLock = async ( - lockKey: string, - timeout: number = 30000 -): Promise<{ release: () => Promise }> => { - const now = Date.now(); - const existing = distributedLocks.get(lockKey); - - // Clean expired locks - if (existing && now > existing.timestamp + existing.timeout) { - distributedLocks.delete(lockKey); - } - - // Wait for lock to be available - let attempts = 0; - while (distributedLocks.has(lockKey) && attempts < 10) { - await sleep(100); - attempts++; - } - - if (distributedLocks.has(lockKey)) { - throw new Error(EvaluationErrEnum.evalLockAcquisitionFailed); - } - - // Acquire lock - distributedLocks.set(lockKey, { timestamp: now, timeout }); - - return { - release: async () => { - distributedLocks.delete(lockKey); - } - }; -}; - -// Enhanced retriable error patterns with categories -const RETRIABLE_ERROR_PATTERNS = { - // Network connectivity issues - network: [ - 'NETWORK_ERROR', - 'ECONNRESET', - 'ENOTFOUND', - 'ECONNREFUSED', - 'Connection refused', - 'socket hang up', - 'connect timeout', - 'EHOSTUNREACH', - 'ENETUNREACH' - ], - // Timeout related errors - timeout: ['TIMEOUT', 'timeout', 'ETIMEDOUT', 'Request timeout', 'Connection timeout'], - // Rate limiting and temporary service issues - rateLimit: [ - 'RATE_LIMIT', - 'rate limit', - 'too many requests', - '429', - 'quota exceeded', - 'throttled' - ], - // Temporary server errors - serverError: [ - '502', - '503', - '504', - 'bad gateway', - 'service unavailable', - 'gateway timeout', - 'temporary failure', - 'server overloaded' - ] -}; - -const maxRetries = Number(process.env.EVAL_ITEM_MAX_RETRY) || 3; // Default max retry count - -// Enhanced error analysis with category detection -const analyzeError = ( - error: any -): { isRetriable: boolean; category?: string; pattern?: string } => { - const errorStr = error?.message || error?.code || String(error); - const lowerErrorStr = errorStr.toLowerCase(); - - // Check each category for matches - for (const [category, patterns] of Object.entries(RETRIABLE_ERROR_PATTERNS)) { - for (const pattern of patterns) { - if (lowerErrorStr.includes(pattern.toLowerCase())) { - return { isRetriable: true, category, pattern }; - } - } - } - - // Check HTTP status codes directly - const httpStatusMatch = errorStr.match(/\b(4\d{2}|5\d{2})\b/); - if (httpStatusMatch) { - const statusCode = httpStatusMatch[1]; - // 4xx errors are generally not retriable except 429 - if (statusCode === '429') { - return { isRetriable: true, category: 'rateLimit', pattern: statusCode }; - } - // 5xx errors are generally retriable - if (statusCode.startsWith('5')) { - return { isRetriable: true, category: 'serverError', pattern: statusCode }; - } - } - - return { isRetriable: false }; -}; - -// Backward compatibility function -const matchesRetriablePattern = (error: any): boolean => { - return analyzeError(error).isRetriable; -}; - -// Determine if target execution error should be retriable -const isTargetExecutionRetriable = (error: any): boolean => { - if (error === TeamErrEnum.aiPointsNotEnough) return false; - return matchesRetriablePattern(error); -}; - -// Determine if evaluator execution error should be retriable -const isEvaluatorExecutionRetriable = (error: any): boolean => { - if (error === TeamErrEnum.aiPointsNotEnough) return false; - return matchesRetriablePattern(error); -}; - -// General error retriability check for handleEvalItemError -const isRetriableError = (error: any): boolean => { - // If it's a structured stage error, use its retriable flag - if (error instanceof EvaluationStageError) { - return error.retriable; - } - - return matchesRetriablePattern(error); -}; - -// Complete evaluation task - simplified version based on status enum statistics -const finishEvaluationTask = async (evalId: string) => { - const lockKey = `eval_task_finish_${evalId}`; - const lock = await acquireDistributedLock(lockKey, 30000); +import type { MetricResult } from '@fastgpt/global/core/evaluation/metric/type'; +import { MetricResultStatusEnum } from '@fastgpt/global/core/evaluation/metric/constants'; +/** + * Complete evaluation task and trigger summary generation + */ +export const finishEvaluationTask = async (evalId: string) => { try { - // Simplified aggregation query: based only on status statistics - const [statsResult] = await MongoEvalItem.aggregate([ - { - $match: { evalId: new Types.ObjectId(evalId) } - }, - { - $group: { - _id: null, - totalCount: { $sum: 1 }, - // Statistics by status - completedCount: { - $sum: { $cond: [{ $eq: ['$status', EvaluationStatusEnum.completed] }, 1, 0] } - }, - errorCount: { - $sum: { $cond: [{ $eq: ['$status', EvaluationStatusEnum.error] }, 1, 0] } - }, - evaluatingCount: { - $sum: { $cond: [{ $eq: ['$status', EvaluationStatusEnum.evaluating] }, 1, 0] } - }, - queuingCount: { - $sum: { $cond: [{ $eq: ['$status', EvaluationStatusEnum.queuing] }, 1, 0] } - } - } - } - ]); + // Get all evaluation items for this task + const allItems = await MongoEvalItem.find({ evalId: new Types.ObjectId(evalId) }, '_id').lean(); - // If no data, return (should not happen) - if (!statsResult) { + if (allItems.length === 0) { addLog.warn(`[Evaluation] Evaluation task has no evaluation item data: ${evalId}`); return; } - const { - totalCount = 0, - completedCount = 0, - errorCount = 0, - evaluatingCount = 0, - queuingCount = 0 - } = statsResult; + const totalCount = allItems.length; + const itemIds = allItems.map((item) => item._id.toString()); + + const statusMap = await getBatchEvaluationItemStatus(itemIds); + + let completedCount = 0; + let errorCount = 0; + let evaluatingCount = 0; + let queuingCount = 0; + + for (const itemId of itemIds) { + const status = statusMap.get(itemId) || EvaluationStatusEnum.completed; + switch (status) { + case EvaluationStatusEnum.completed: + completedCount++; + break; + case EvaluationStatusEnum.error: + errorCount++; + break; + case EvaluationStatusEnum.evaluating: + evaluatingCount++; + break; + case EvaluationStatusEnum.queuing: + queuingCount++; + break; + } + } - // Check if truly completed + // Check if all items are truly completed const pendingCount = evaluatingCount + queuingCount; - // Task status is always completed when all items are finished - let taskStatus: EvaluationStatusEnum = EvaluationStatusEnum.completed; if (pendingCount > 0) { addLog.debug( - `[Evaluation] Task not yet completed: ${evalId}, pending items: ${pendingCount}` + `[Evaluation] Task still has pending items, skipping completion: ${evalId}, total: ${totalCount}, ` + + `success: ${completedCount}, failed: ${errorCount}, pending: ${pendingCount}` ); - taskStatus = EvaluationStatusEnum.evaluating; - } - - // Update task status with statistical fields - const updateFields: any = { - status: taskStatus, - // Use statistics object to store execution statistics - statistics: { - totalItems: totalCount, - completedItems: completedCount, - errorItems: errorCount - } - }; - - // Only set finishTime if the task is actually completed - if (taskStatus === EvaluationStatusEnum.completed) { - updateFields.finishTime = new Date(); + return; } - await MongoEvaluation.updateOne({ _id: new Types.ObjectId(evalId) }, { $set: updateFields }); - - addLog.debug( - `[Evaluation] Task completed: ${evalId}, status: ${taskStatus}, total: ${totalCount}, ` + - `success: ${completedCount}, failed: ${errorCount}` + // Set finishTime if all items are finished (either completed or error, no pending) + // Use conditional update to prevent duplicate writes + const updateResult = await MongoEvaluation.updateOne( + { + _id: new Types.ObjectId(evalId), + finishTime: { $exists: false } // Only update if finishTime is not already set + }, + { $set: { finishTime: new Date() } } ); - // Trigger async summary generation only for metrics with empty summaries if task completed successfully - if (taskStatus === EvaluationStatusEnum.completed && completedCount > 0) { - try { - // Get current evaluation to extract metric IDs and check summary status - const currentEvaluation = await MongoEvaluation.findById( - evalId, - 'evaluators summaryConfigs' - ).lean(); - - if (currentEvaluation?.evaluators && currentEvaluation.evaluators.length > 0) { - // Filter metrics that have empty summaries - const metricsNeedingSummary: string[] = []; - - currentEvaluation.evaluators.forEach((evaluator: any, index: number) => { - const metricId = evaluator.metric._id.toString(); - const summaryConfig = currentEvaluation.summaryConfigs[index]; - - // Check if summary is empty or null - if (!summaryConfig?.summary || summaryConfig.summary.trim() === '') { - metricsNeedingSummary.push(metricId); - } - }); - - if (metricsNeedingSummary.length > 0) { - // Trigger async summary generation only for metrics with empty summaries (fire and forget) - setImmediate(() => { - EvaluationSummaryService.generateSummaryReports(evalId, metricsNeedingSummary).catch( - (error) => { - addLog.error( - `[Evaluation] Failed to trigger async summary generation: ${evalId}`, - error - ); - } - ); - }); - - addLog.debug( - `[Evaluation] Triggered async summary generation for ${metricsNeedingSummary.length} metrics with empty summaries: ${evalId}` - ); - } else { - addLog.debug( - `[Evaluation] All metrics already have summaries, skipping summary generation: ${evalId}` - ); - } - } - } catch (summaryError) { - // Don't affect main task completion flow, just log the error - addLog.warn(`[Evaluation] Failed to trigger summary generation: ${evalId}`, { - error: summaryError instanceof Error ? summaryError.message : String(summaryError) - }); - } + // If no document was modified, it means another process already finished the task + if (updateResult.modifiedCount === 0) { + addLog.debug( + `[Evaluation] Task already finished by another process: ${evalId}, skipping completion` + ); + return; } + + // Trigger summary generation for completed task + await EvaluationSummaryService.triggerSummaryGeneration(evalId, completedCount); } catch (error) { - addLog.error(`[Evaluation] Error occurred while completing task: ${evalId}`, error); + addLog.error(`[Evaluation] Error occurred while completing task: ${evalId}`, { + error: getErrText(error) + }); - // When error occurs, mark task as error status + // Save error info to database try { await MongoEvaluation.updateOne( { _id: new Types.ObjectId(evalId) }, { $set: { - status: EvaluationStatusEnum.error, finishTime: new Date(), - errorMessage: `System error occurred while completing task: ${error instanceof Error ? error.message : 'Unknown error'}` + errorMessage: EvaluationErrEnum.evalTaskSystemError } } ); } catch (updateError) { - addLog.error(`[Evaluation] Failed to update task error status: ${evalId}`, updateError); + addLog.warn(`[Evaluation] Failed to update task error info: ${evalId}`, { + updateError: getErrText(updateError) + }); } - } finally { - await lock.release(); - } -}; - -// Handle evaluation item error -const handleEvalItemError = async (evalItemId: string, evalId: string, error: any) => { - let errorMessage = getErrText(error); - let stage = 'Unknown'; - - // Extract stage and error information from structured errors - if (error instanceof EvaluationStageError) { - stage = error.stage; - errorMessage = `[${stage}] ${error.message}`; - } - - // Get current evaluation item - const evalItem = await MongoEvalItem.findById(evalItemId, 'retry evalId'); - if (!evalItem) { - addLog.error(`[Evaluation] Evaluation item does not exist: ${evalItemId}`); - return; - } - - const isRetriable = isRetriableError(error); - const currentRetryCount = evalItem.retry || 0; - const newRetryCount = isRetriable ? Math.max(currentRetryCount - 1, 0) : 0; - const shouldRetry = isRetriable && newRetryCount > 0; - const newStatus = shouldRetry ? EvaluationStatusEnum.queuing : EvaluationStatusEnum.error; - - // Build retry attempt info for logging - const retryAttempt = maxRetries - currentRetryCount + 1; - - const updateData: any = { - retry: newRetryCount, - errorMessage, - status: newStatus, - finishTime: newStatus === EvaluationStatusEnum.error ? new Date() : undefined - }; - - await MongoEvalItem.updateOne({ _id: new Types.ObjectId(evalItemId) }, updateData); - - // Re-enqueue for retry with improved job naming - if (shouldRetry) { - const retryDelay = Math.min(1000 * Math.pow(2, maxRetries - newRetryCount), 30000); // Exponential backoff - await evaluationItemQueue.add( - `eval_item_${evalItemId}_retry_${retryAttempt}`, - { - evalId, - evalItemId - }, - { - delay: retryDelay - } - ); - - addLog.debug( - `[Evaluation] Item requeued for retry: ${evalItemId}, stage: ${stage}, remaining: ${newRetryCount}, delay: ${retryDelay}ms` - ); - } else { - addLog.error( - `[Evaluation] Item failed permanently: ${evalItemId}, stage: ${stage}, retriable: ${isRetriable}`, - error instanceof EvaluationStageError ? error.originalError || error : error - ); } }; -// Evaluation task processor +/** + * Process evaluation task: validate config and submit items to queue + */ const evaluationTaskProcessor = async (job: Job) => { const { evalId } = job.data; - addLog.debug(`[Evaluation] Start processing evaluation task: ${evalId}`); - - try { - // Get evaluation task information - const evaluation = await MongoEvaluation.findById(evalId).lean(); - if (!evaluation) { - addLog.warn(`[Evaluation] Evaluation task does not exist: ${evalId}`); - return; - } + // Report progress + await job.updateProgress(0); - // Load dataset - const dataItems = await MongoEvalDatasetData.find({ - datasetId: evaluation.datasetId, - teamId: evaluation.teamId - }).lean(); - - // TODO: Handle targetCallParams population for evaluation data items - // The dataItems loaded from dataset only contain basic EvalDatasetDataSchemaType fields - // but evaluation items need EvaluationDataItemType (including targetCallParams). - // Need to: - // 1. Determine source of targetCallParams (evaluation config, dataset metadata, or default) - // 2. Transform dataItems to include targetCallParams before creating evaluation items - // 3. Consider caching strategy for targetCallParams if they are dynamic per evaluation - - if (dataItems.length === 0) { - throw new Error(EvaluationErrEnum.evalDatasetLoadFailed); - } + // Get evaluation data + const evaluation = await MongoEvaluation.findById(evalId).lean(); - // Validate target and evaluators configuration - if (!evaluation.target || !evaluation.target.type || !evaluation.target.config) { - throw new Error(EvaluationErrEnum.evalTargetConfigInvalid); - } + // Skip if task doesn't exist + if (!evaluation) { + addLog.warn(`[Evaluation] Task ${evalId} no longer exists, skipping`); + return; + } - if (!evaluation.evaluators || evaluation.evaluators.length === 0) { - throw new Error(EvaluationErrEnum.evalEvaluatorsConfigInvalid); - } + addLog.debug(`[Evaluation] Task ${evalId} now evaluating`); - // Check if evaluation items already exist (reentrant handling) - const existingItems = await MongoEvalItem.find({ evalId }).lean(); - if (existingItems.length > 0) { - addLog.debug(`[Evaluation] Task already has ${existingItems.length} items, resuming...`); + // Validate target and evaluators configuration + if (!evaluation.target || !evaluation.target.type || !evaluation.target.config) { + throw createEvaluationError(EvaluationErrEnum.evalTargetConfigInvalid, 'ResourceCheck'); + } - // Re-submit unfinished items to queue - const pendingItems = existingItems.filter( - (item) => - item.status === EvaluationStatusEnum.queuing || - (item.status === EvaluationStatusEnum.error && item.retry > 0) - ); + if (!evaluation.evaluators || evaluation.evaluators.length === 0) { + throw createEvaluationError(EvaluationErrEnum.evalEvaluatorsConfigInvalid, 'ResourceCheck'); + } - if (pendingItems.length > 0) { - const jobs = pendingItems.map((item, index) => ({ - name: `eval_item_${evalId}_resume_${index}`, - data: { - evalId, - evalItemId: item._id.toString() - }, - opts: { - delay: index * 100 - } - })); + // Report validation progress + await job.updateProgress(20); - await evaluationItemQueue.addBulk(jobs); - addLog.debug(`[Evaluation] Resumed ${jobs.length} pending items`); - } - return; - } + // Check if evaluation items exist + const existingItems = await MongoEvalItem.find({ evalId }).lean(); + if (existingItems.length === 0) { + throw createEvaluationError(EvaluationErrEnum.evalItemNotFound, 'ResourceCheck'); + } - // Create evaluation items for each dataItem and each evaluator (atomic structure) - const evalItems = []; - for (const dataItem of dataItems) { - for (const evaluator of evaluation.evaluators) { - evalItems.push({ - evalId, - dataItem, - target: evaluation.target, - evaluator, - status: EvaluationStatusEnum.queuing, - retry: maxRetries - }); - } - } + // Get real-time status and submit items to queue + const itemIds = existingItems.map((item) => item._id.toString()); + const statusMap = await getBatchEvaluationItemStatus(itemIds); - // Batch insert evaluation items - const insertedItems = await MongoEvalItem.insertMany(evalItems); - addLog.debug(`[Evaluation] Created ${insertedItems.length} atomic evaluation items`); + const itemsToProcess = existingItems.filter((item) => { + const realTimeStatus = statusMap.get(item._id.toString()) || EvaluationStatusEnum.completed; + // Only process items in queuing status + return realTimeStatus === EvaluationStatusEnum.queuing; + }); - // Submit to evaluation item queue for concurrent processing - const jobs = insertedItems.map((item, index) => ({ - name: `eval_item_${evalId}_${index}`, + if (itemsToProcess.length > 0) { + const jobs = itemsToProcess.map((item, index) => ({ data: { evalId, evalItemId: item._id.toString() }, - opts: { - delay: index * 100 // Add small delay to avoid starting too many tasks simultaneously - } + delay: index * 100 // Small delay to avoid overwhelming system })); - await evaluationItemQueue.addBulk(jobs); + await addEvaluationItemJobs(jobs); + addLog.debug(`[Evaluation] Submitted ${jobs.length} items to queue`); + } - addLog.debug( - `[Evaluation] Task decomposition completed: ${evalId}, submitted ${jobs.length} evaluation items to queue` - ); - } catch (error) { - addLog.error(`[Evaluation] Task processing failed: ${evalId}`, error); + // Report completion + await job.updateProgress(100); - // Mark task as failed - await MongoEvaluation.updateOne( - { _id: new Types.ObjectId(evalId) }, - { - $set: { - errorMessage: getErrText(error), - status: EvaluationStatusEnum.error, - finishTime: new Date() - } - } - ); - } + addLog.debug( + `[Evaluation] Task processing completed: ${evalId}, submitted ${itemsToProcess.length} evaluation items to queue` + ); }; -// Evaluation item processor +/** + * Process evaluation item: execute target and evaluators + */ const evaluationItemProcessor = async (job: Job) => { const { evalId, evalItemId } = job.data; addLog.debug(`[Evaluation] Start processing evaluation item: ${evalItemId}`); - try { - // Get evaluation item information - const evalItem = await MongoEvalItem.findById(evalItemId); - if (!evalItem) { - throw new EvaluationStageError( - EvaluationStageEnum.ResourceCheck, - getErrText(EvaluationErrEnum.evalItemNotFound), - false // Resource not found errors are not retriable - ); - } + // Report progress + await job.updateProgress(0); - // Check if item is already completed (reentrant handling) - if (evalItem.status === EvaluationStatusEnum.completed) { - addLog.debug(`[Evaluation] Item already completed: ${evalItemId}`); - return; - } + // Get evaluation item + const evalItem = await MongoEvalItem.findById(evalItemId); + if (!evalItem) { + throw createEvaluationError(EvaluationErrEnum.evalItemNotFound, 'ResourceCheck'); + } - // Get evaluation information for AI Points check - const evaluation = await MongoEvaluation.findById(evalId, 'teamId tmbId usageId'); - if (!evaluation) { - throw new EvaluationStageError( - EvaluationStageEnum.ResourceCheck, - getErrText(EvaluationErrEnum.evalTaskNotFound), - false // Resource not found errors are not retriable - ); - } + // Get evaluation for AI points check and configuration + const evaluation = await MongoEvaluation.findById( + evalId, + 'teamId tmbId usageId target evaluators' + ); + if (!evaluation) { + throw createEvaluationError(EvaluationErrEnum.evalTaskNotFound, 'ResourceCheck'); + } - // Check AI Points - try { - await checkTeamAIPoints(evaluation.teamId); - } catch (error) { - throw new EvaluationStageError( - EvaluationStageEnum.ResourceCheck, - getErrText(error), - false // AI Point errors are not retriable - ); - } + // Check AI points availability + try { + await checkTeamAIPoints(evaluation.teamId); + } catch (error) { + throw createEvaluationError(error, 'ResourceCheck'); + } - // Initialize outputs - check for existing results first for resume capability - let targetOutput: any = undefined; - let evaluatorOutput: any = undefined; + // Initialize outputs and check for existing results + let targetOutput: TargetOutput | undefined = undefined; + let evaluatorOutputs: MetricResult[] = []; - // Resume from checkpoint only if in evaluating status - if (evalItem.status === EvaluationStatusEnum.evaluating) { - if (evalItem.targetOutput?.actualOutput) { - addLog.debug(`[Evaluation] Resuming targetOutput from evalItem: ${evalItemId}`); - targetOutput = evalItem.targetOutput; - } - if (evalItem.evaluatorOutput?.data?.score) { - addLog.debug(`[Evaluation] Resuming evaluatorOutput from evalItem: ${evalItemId}`); - evaluatorOutput = evalItem.evaluatorOutput; - } - } else { - // For queuing or error status, always start from scratch - addLog.debug( - `[Evaluation] Starting/restarting item from scratch: ${evalItemId}, status: ${evalItem.status}` - ); - } + // Resume from checkpoint if results exist + if (evalItem.targetOutput?.actualOutput) { + addLog.debug(`[Evaluation] Resuming targetOutput from evalItem: ${evalItemId}`); + targetOutput = evalItem.targetOutput; + } + if (evalItem.evaluatorOutputs && evalItem.evaluatorOutputs.length > 0) { + addLog.debug(`[Evaluation] Resuming evaluatorOutputs from evalItem: ${evalItemId}`); + evaluatorOutputs = evalItem.evaluatorOutputs; + } - // Update status to processing - await MongoEvalItem.updateOne( - { _id: new Types.ObjectId(evalItemId) }, - { $set: { status: EvaluationStatusEnum.evaluating } } - ); + if (!targetOutput && !evaluatorOutputs.length) { + addLog.debug(`[Evaluation] Starting evaluation item from scratch: ${evalItemId}`); + } - // 1. Call evaluation target (if not already done) - if (!targetOutput || !targetOutput.actualOutput) { - try { - const targetInstance = await createTargetInstance(evalItem.target, { validate: false }); - targetOutput = await targetInstance.execute({ - userInput: evalItem.dataItem.userInput, - context: evalItem.dataItem.context, - targetCallParams: evalItem.dataItem.targetCallParams - }); + // Report setup progress + await job.updateProgress(10); - // Save target output as checkpoint - await MongoEvalItem.updateOne( - { _id: new Types.ObjectId(evalItemId) }, - { $set: { targetOutput: targetOutput } } - ); + // Execute evaluation target if needed + if (!targetOutput || !targetOutput.actualOutput) { + try { + const targetInstance = await createTargetInstance(evaluation.target, { validate: false }); + targetOutput = await targetInstance.execute({ + userInput: evalItem.dataItem.userInput, + context: evalItem.dataItem.context, + targetCallParams: evalItem.dataItem.targetCallParams + }); - // Record usage from target call - if (targetOutput.usage) { - const totalPoints = targetOutput.usage.reduce( - (sum: number, item: any) => sum + (item.totalPoints || 0), - 0 - ); - await createMergedEvaluationUsage({ - evalId, - teamId: evaluation.teamId, - tmbId: evaluation.tmbId, - usageId: evaluation.usageId, - totalPoints, - type: 'target' - }); + // Save target output as checkpoint + await MongoEvalItem.updateOne( + { _id: new Types.ObjectId(evalItemId) }, + { + $set: { + targetOutput: targetOutput + } } - } catch (error) { - // Normalize target execution error - const retriable = isTargetExecutionRetriable(error); - const errorMessage = getErrText(error) || 'Target execution failed'; - - throw new EvaluationStageError( - EvaluationStageEnum.TaskExecute, - errorMessage, - retriable, - error - ); - } - } + ); - // 2. Execute evaluator (if not already done) - let totalMetricPoints = 0; + // Report target execution progress + await job.updateProgress(30); - if (!evaluatorOutput || !evaluatorOutput.data?.score) { - try { - const evaluatorInstance = await createEvaluatorInstance(evalItem.evaluator, { - validate: false + // Record target usage + if (targetOutput.usage) { + const totalPoints = targetOutput.usage.reduce( + (sum: number, item: any) => sum + (item.totalPoints || 0), + 0 + ); + const inputTokens = targetOutput.usage.reduce( + (sum: number, item: any) => sum + (item.inputTokens || 0), + 0 + ); + const outputTokens = targetOutput.usage.reduce( + (sum: number, item: any) => sum + (item.outputTokens || 0), + 0 + ); + await createMergedEvaluationUsage({ + evalId, + teamId: evaluation.teamId, + tmbId: evaluation.tmbId, + usageId: evaluation.usageId, + totalPoints, + type: 'target', + inputTokens, + outputTokens }); + } - evaluatorOutput = await evaluatorInstance.evaluate({ - userInput: evalItem.dataItem.userInput, - expectedOutput: evalItem.dataItem.expectedOutput, - actualOutput: targetOutput.actualOutput, - context: evalItem.dataItem.context, - retrievalContext: targetOutput.retrievalContext - }); - } catch (error) { - // Normalize evaluator execution error - const retriable = isEvaluatorExecutionRetriable(error); - const errorMessage = getErrText(error) || 'Evaluator execution failed'; - - throw new EvaluationStageError( - EvaluationStageEnum.EvaluatorExecute, - errorMessage, - retriable, - error - ); + if (!targetOutput.actualOutput) { + throw new Error(EvaluationErrEnum.evalTargetExecutionError); } + } catch (error) { + // Use BullMQ error type for retry handling + throw createEvaluationError(error, 'TargetExecute', { + evalId, + evalItemId + }); } + } + + // Execute evaluators (only missing ones) + while (evaluatorOutputs.length < evaluation.evaluators.length) { + const evaluatorIndex = evaluatorOutputs.length; + evaluatorOutputs.push({ + metricName: evaluation.evaluators[evaluatorIndex].metric.name + }); + } + + const errors: Array<{ evaluatorName: string; error: string }> = []; - // Record usage from metric evaluation - if (evaluatorOutput.totalPoints) { - totalMetricPoints += evaluatorOutput.totalPoints || 0; + // Process each evaluator + for (let i = 0; i < evaluation.evaluators.length; i++) { + const evaluator = evaluation.evaluators[i]; + const existingOutput = evaluatorOutputs[i]; + + // Skip if evaluator already has valid successful result + if ( + existingOutput?.data?.score !== undefined && + existingOutput?.status === MetricResultStatusEnum.Success + ) { + continue; } - // Record usage from metric evaluation - if (totalMetricPoints > 0) { + try { + const evaluatorInstance = await createEvaluatorInstance(evaluator, { + validate: false + }); + + const evaluatorOutput = await evaluatorInstance.evaluate({ + userInput: evalItem.dataItem.userInput, + expectedOutput: evalItem.dataItem.expectedOutput, + actualOutput: targetOutput.actualOutput, + context: evalItem.dataItem.context, + retrievalContext: targetOutput.retrievalContext + }); + await createMergedEvaluationUsage({ evalId, teamId: evaluation.teamId, tmbId: evaluation.tmbId, usageId: evaluation.usageId, - totalPoints: totalMetricPoints, + totalPoints: evaluatorOutput.totalPoints || 0, + inputTokens: + evaluatorOutput.usages?.reduce((sum, usage) => sum + (usage.promptTokens || 0), 0) || 0, + outputTokens: + evaluatorOutput.usages?.reduce((sum, usage) => sum + (usage.completionTokens || 0), 0) || + 0, type: 'metric' }); - } - // 3. Store results - await MongoEvalItem.updateOne( - { _id: new Types.ObjectId(evalItemId) }, - { - $set: { - targetOutput: targetOutput, - evaluatorOutput: evaluatorOutput, - status: EvaluationStatusEnum.completed, - finishTime: new Date() - } + // Record error and continue + if (evaluatorOutput.status !== MetricResultStatusEnum.Success || evaluatorOutput.error) { + const errorMessage = evaluatorOutput.error || 'Evaluator execution failed'; + const evaluatorName = evaluator.metric.name || `Evaluator ${i + 1}`; + errors.push({ evaluatorName, error: errorMessage }); } - ); - addLog.debug( - `[Evaluation] Evaluation item completed: ${evalItemId}, score: ${evaluatorOutput?.data?.score}` - ); - } catch (error) { - addLog.error(`[Evaluation] Evaluation item error: ${evalItemId}, error: ${error}`); - await handleEvalItemError(evalItemId, evalId, error); + // Update evaluator output + evaluatorOutputs[i] = evaluatorOutput; + + // Save evaluator progress + await MongoEvalItem.updateOne( + { _id: new Types.ObjectId(evalItemId) }, + { $set: { evaluatorOutputs: evaluatorOutputs } } + ); + + // Report evaluator progress + const completedEvaluators = evaluatorOutputs.filter( + (output) => output?.data?.score !== undefined + ).length; + const evaluatorProgress = 30 + (60 * completedEvaluators) / evaluation.evaluators.length; + await job.updateProgress(Math.round(evaluatorProgress)); + } catch (error) { + // Handle evaluator error + const errorMessage = getErrText(error) || 'Evaluator execution failed'; + const evaluatorName = evaluator.metric.name || `Evaluator ${i + 1}`; + errors.push({ evaluatorName, error: errorMessage }); + } } - // After try-catch, check if all evaluation items are completed - try { - await finishEvaluationTask(evalId); - } catch (finishError) { - addLog.error( - `[Evaluation] Error occurred while checking task completion status: ${evalId}`, - finishError + // Check for evaluator errors + if (errors.length > 0) { + const errorDetails = errors.map((e) => `${e.evaluatorName}: ${e.error}`).join('; '); + addLog.error('[Evaluation] Multiple evaluator execution errors', { + errorCount: errors.length, + details: errorDetails, + errors: errors + }); + + // Use BullMQ error type + throw createEvaluationError( + EvaluationErrEnum.evalEvaluatorExecutionErrors, + 'EvaluatorExecute', + { + evalId, + evalItemId + }, + true ); } + + // Report final progress + await job.updateProgress(100); }; -// Initialize worker +/** + * Initialize evaluation task workers + */ export const initEvalTaskWorker = () => { return getEvaluationTaskWorker(evaluationTaskProcessor); }; @@ -733,5 +418,7 @@ export const initEvalTaskItemWorker = () => { return getEvaluationItemWorker(evaluationItemProcessor); }; -// Export for testing -export { evaluationTaskProcessor, evaluationItemProcessor, finishEvaluationTask }; +/** + * Export processors for testing + */ +export { evaluationTaskProcessor, evaluationItemProcessor }; diff --git a/packages/service/core/evaluation/task/schema.ts b/packages/service/core/evaluation/task/schema.ts index f4555b753979..655ee61eaa6a 100644 --- a/packages/service/core/evaluation/task/schema.ts +++ b/packages/service/core/evaluation/task/schema.ts @@ -5,8 +5,10 @@ import { import { connectionMongo, getMongoModel } from '../../../common/mongo'; import type { EvaluationSchemaType, - EvaluationItemSchemaType + EvaluationItemSchemaType, + TargetOutput } from '@fastgpt/global/core/evaluation/type'; +import { EvalDatasetDataKeyEnum } from '@fastgpt/global/core/evaluation/dataset/constants'; import { UsageCollectionName } from '../../../support/wallet/usage/schema'; import { EvaluationStatusEnum, @@ -65,12 +67,12 @@ export const EvaluationEvaluatorSchema = new Schema( thresholdValue: { type: Number, required: false, - default: 80 + default: 0.8 }, scoreScaling: { type: Number, required: false, - default: 100, // Default 100x amplification + default: 1, // Default no scaling validate: { validator: function (value: number) { return ( @@ -91,7 +93,9 @@ export const EvaluationEvaluatorSchema = new Schema( } ); -// Collection names +/** + * MongoDB collection names + */ export const EvaluationCollectionName = 'evals'; export const EvalItemCollectionName = 'eval_items'; @@ -118,7 +122,7 @@ export const EvaluationTaskSchema = new Schema({ trim: true, maxlength: 100 }, - datasetId: { + evalDatasetCollectionId: { type: Schema.Types.ObjectId, ref: EvalDatasetCollectionName, required: true @@ -130,11 +134,6 @@ export const EvaluationTaskSchema = new Schema({ ref: UsageCollectionName, required: true }, - status: { - type: Number, - enum: EvaluationStatusValues, - default: EvaluationStatusEnum.queuing - }, createTime: { type: Date, required: true, @@ -142,22 +141,7 @@ export const EvaluationTaskSchema = new Schema({ }, finishTime: Date, errorMessage: String, - // Statistical information - statistics: { - totalItems: { - type: Number, - default: 0 - }, - completedItems: { - type: Number, - default: 0 - }, - errorItems: { - type: Number, - default: 0 - } - }, - // Summary configuration for each evaluator (indexed by evaluator index) + // Summary configuration for each evaluator summaryConfigs: [ { metricId: { @@ -173,7 +157,7 @@ export const EvaluationTaskSchema = new Schema({ required: true }, calculateType: { - type: Number, + type: String, enum: CaculateMethodValues, required: true }, @@ -182,7 +166,7 @@ export const EvaluationTaskSchema = new Schema({ default: '' }, summaryStatus: { - type: Number, + type: String, enum: SummaryStatusValues, default: SummaryStatusEnum.pending }, @@ -195,62 +179,66 @@ export const EvaluationTaskSchema = new Schema({ ] }); -// Optimized indexes for EvaluationTaskSchema -EvaluationTaskSchema.index({ teamId: 1, createTime: -1 }); // Main query: team filtering + time sorting -EvaluationTaskSchema.index({ teamId: 1, status: 1, createTime: -1 }); // Status filtering + time sorting -EvaluationTaskSchema.index({ teamId: 1, name: 1 }, { unique: true }); // Name uniqueness check -EvaluationTaskSchema.index({ tmbId: 1, createTime: -1 }); // Query by creator -EvaluationTaskSchema.index({ status: 1 }); // Status-based queue processing +/** + * Optimized indexes based on query patterns + */ +EvaluationTaskSchema.index({ _id: 1, teamId: 1 }); // Primary lookup +EvaluationTaskSchema.index({ teamId: 1, createTime: -1 }); // Team listing with time sort +EvaluationTaskSchema.index({ teamId: 1, tmbId: 1, createTime: -1 }); // Permission filtering +EvaluationTaskSchema.index({ teamId: 1, name: 1 }, { unique: true }); // Name uniqueness -// Atomic evaluation item: one dataItem + one target + one evaluator +/** + * Evaluation item schema: atomic unit for evaluation + */ export const EvaluationItemSchema = new Schema({ evalId: { type: Schema.Types.ObjectId, ref: EvaluationCollectionName, required: true }, - // Dependent component configurations + // Data item configuration dataItem: { type: Object, required: true }, - target: EvaluationTargetSchema, - evaluator: EvaluationEvaluatorSchema, // Single evaluator configuration - // Execution results + // Execution results and outputs targetOutput: { type: Schema.Types.Mixed, default: {} }, - evaluatorOutput: { - type: Schema.Types.Mixed, - default: {} - }, - status: { - type: Number, - default: EvaluationStatusEnum.queuing, - enum: EvaluationStatusValues - }, - retry: { - type: Number, - default: 3 + evaluatorOutputs: { + type: [Schema.Types.Mixed], + default: [] }, finishTime: Date, - errorMessage: String + errorMessage: String, + // Metadata for optimization + metadata: { + status: { + type: String, + enum: EvaluationStatusValues, + default: EvaluationStatusEnum.queuing, + index: true // Index for status filtering + } + } }); -// Optimized indexes for EvaluationItemSchema -EvaluationItemSchema.index({ evalId: 1, createTime: -1 }); // Main query: eval filtering + time sorting -EvaluationItemSchema.index({ evalId: 1, status: 1, createTime: -1 }); // Status filtering + time sorting -EvaluationItemSchema.index({ status: 1, retry: 1 }); // Queue processing with retry logic -EvaluationItemSchema.index({ evalId: 1, 'dataItem._id': 1 }); // DataItem aggregation queries -EvaluationItemSchema.index({ evalId: 1, status: 1, retry: 1 }); // Retry operations optimization +/** + * Evaluation item indexes for performance + */ +EvaluationItemSchema.index({ evalId: 1 }); // Basic queries +EvaluationItemSchema.index({ evalId: 1, createTime: -1 }); // Time-sorted listing + +// Status filtering indexes +EvaluationItemSchema.index({ evalId: 1, 'metadata.status': 1, createTime: -1 }); // Status with time +EvaluationItemSchema.index({ evalId: 1, 'metadata.status': 1 }); // Status only -// Optimized text search for content filtering (removed evalId for flexibility) +// Text search index for content filtering EvaluationItemSchema.index({ 'dataItem.userInput': 'text', 'dataItem.expectedOutput': 'text', 'targetOutput.actualOutput': 'text' -}); // Comprehensive text search across all content fields +}); // Text search across inputs and outputs export const MongoEvaluation = getMongoModel( EvaluationCollectionName, diff --git a/packages/service/core/evaluation/task/statusCalculator.ts b/packages/service/core/evaluation/task/statusCalculator.ts new file mode 100644 index 000000000000..f2a899411346 --- /dev/null +++ b/packages/service/core/evaluation/task/statusCalculator.ts @@ -0,0 +1,427 @@ +import { addLog } from '../../../common/system/log'; +import { EvaluationStatusEnum } from '@fastgpt/global/core/evaluation/constants'; +import { evaluationTaskQueue, evaluationItemQueue } from './mq'; +import { MongoEvaluation, MongoEvalItem } from './schema'; +import { Types } from 'mongoose'; + +/** + * Evaluation task status calculator + * Calculates real-time status from job queues, not database status fields + */ +export async function getEvaluationTaskStatus(evalId: string): Promise { + try { + // Get task-related jobs + const taskJobs = await evaluationTaskQueue.getJobs([ + 'waiting', + 'active', + 'delayed', + 'failed', + 'completed' + ]); + + const relatedTaskJobs = taskJobs.filter((job) => job.data.evalId === evalId); + + // If no task jobs, check evaluation item jobs + if (relatedTaskJobs.length === 0) { + return await getEvaluationTaskStatusFromItems(evalId); + } + + // Get job states and prioritize by importance + const jobStates = await Promise.all(relatedTaskJobs.map(async (job) => await job.getState())); + + // Return status by priority: evaluating > error > queuing > completed + if (jobStates.includes('active')) { + return EvaluationStatusEnum.evaluating; + } + + if (jobStates.includes('failed')) { + return EvaluationStatusEnum.error; + } + + if (jobStates.some((state) => ['waiting', 'delayed', 'prioritized'].includes(state))) { + return EvaluationStatusEnum.queuing; + } + + // If all task jobs completed, check evaluation item status + return await getEvaluationTaskStatusFromItems(evalId); + } catch (error) { + return EvaluationStatusEnum.error; + } +} + +/** + * Calculate evaluation task status from evaluation item jobs + */ +async function getEvaluationTaskStatusFromItems(evalId: string): Promise { + try { + const itemJobs = await evaluationItemQueue.getJobs([ + 'waiting', + 'active', + 'delayed', + 'failed', + 'completed' + ]); + + const relatedItemJobs = itemJobs.filter((job) => job.data.evalId === evalId); + + // If no evaluation item jobs, check if task is completed via finishTime + if (relatedItemJobs.length === 0) { + try { + const evaluation = await MongoEvaluation.findById(new Types.ObjectId(evalId), { + finishTime: 1 + }); + if (evaluation?.finishTime) { + return EvaluationStatusEnum.completed; + } + return EvaluationStatusEnum.queuing; + } catch { + return EvaluationStatusEnum.queuing; + } + } + + // Get job states + const itemJobStates = await Promise.all( + relatedItemJobs.map(async (job) => await job.getState()) + ); + + // Return status by priority: evaluating > error > queuing > completed + if (itemJobStates.includes('active')) { + return EvaluationStatusEnum.evaluating; + } + + if (itemJobStates.includes('failed')) { + return EvaluationStatusEnum.error; + } + + if (itemJobStates.some((state) => ['waiting', 'delayed', 'prioritized'].includes(state))) { + return EvaluationStatusEnum.queuing; + } + + if (itemJobStates.includes('completed')) { + return EvaluationStatusEnum.completed; + } + + // Default status + return EvaluationStatusEnum.completed; + } catch (error) { + return EvaluationStatusEnum.error; + } +} + +/** + * Calculate real-time status of evaluation item + */ +export async function getEvaluationItemStatus(evalItemId: string): Promise { + try { + const itemJobs = await evaluationItemQueue.getJobs([ + 'waiting', + 'active', + 'delayed', + 'failed', + 'completed' + ]); + + const relatedJobs = itemJobs.filter((job) => job.data.evalItemId === evalItemId); + + // If no related jobs, check database status to determine if queuing or completed + if (relatedJobs.length === 0) { + try { + const evalItem = await MongoEvalItem.findById(new Types.ObjectId(evalItemId), { + finishTime: 1, + errorMessage: 1 + }); + if (evalItem?.finishTime) { + return evalItem.errorMessage + ? EvaluationStatusEnum.error + : EvaluationStatusEnum.completed; + } + return EvaluationStatusEnum.queuing; + } catch { + return EvaluationStatusEnum.queuing; + } + } + + // Get job states,取最高优先级状态 + const jobStates = await Promise.all(relatedJobs.map(async (job) => await job.getState())); + + // Return status by priority: evaluating > error > queuing > completed + if (jobStates.includes('active')) { + return EvaluationStatusEnum.evaluating; + } + + if (jobStates.includes('failed')) { + return EvaluationStatusEnum.error; + } + + if (jobStates.some((state) => ['waiting', 'delayed', 'prioritized'].includes(state))) { + return EvaluationStatusEnum.queuing; + } + + if (jobStates.includes('completed')) { + return EvaluationStatusEnum.completed; + } + + return EvaluationStatusEnum.queuing; + } catch (error) { + return EvaluationStatusEnum.error; + } +} + +/** + * Batch calculate evaluation item status for performance optimization + */ +export async function getBatchEvaluationItemStatus( + evalItemIds: string[] +): Promise> { + const statusMap = new Map(); + + try { + // Get all related jobs in single query to reduce queries + const itemJobs = await evaluationItemQueue.getJobs([ + 'waiting', + 'active', + 'delayed', + 'failed', + 'completed' + ]); + + // Query database status first to distinguish queuing vs completed + const evalItems = await MongoEvalItem.find( + { _id: { $in: evalItemIds.map((id) => new Types.ObjectId(id)) } }, + { finishTime: 1, errorMessage: 1 } + ); + + const itemStatusByDb = new Map(); + evalItems.forEach((item) => { + const itemId = item._id.toString(); + if (item.finishTime) { + itemStatusByDb.set( + itemId, + item.errorMessage ? EvaluationStatusEnum.error : EvaluationStatusEnum.completed + ); + } else { + itemStatusByDb.set(itemId, EvaluationStatusEnum.queuing); + } + }); + + // Initialize default status for each evalItemId based on database status + evalItemIds.forEach((id) => { + statusMap.set(id, itemStatusByDb.get(id) || EvaluationStatusEnum.queuing); + }); + + // Group jobs by evalItemId and batch get states + const jobsByItemId = new Map(); + itemJobs.forEach((job) => { + if (evalItemIds.includes(job.data.evalItemId)) { + const itemId = job.data.evalItemId; + if (!jobsByItemId.has(itemId)) { + jobsByItemId.set(itemId, []); + } + jobsByItemId.get(itemId)!.push(job); + } + }); + + // Optimize: batch get all job states to reduce async calls + const allJobsToCheck = Array.from(jobsByItemId.values()).flat(); + const allJobStates = await Promise.all( + allJobsToCheck.map(async (job) => ({ + job, + state: await job.getState() + })) + ); + + // Create job to state mapping + const jobStateMap = new Map(); + allJobStates.forEach(({ job, state }) => { + jobStateMap.set(job, state); + }); + + // Calculate status for each evaluation item (prioritize job status if exists) + for (const [itemId, jobs] of jobsByItemId.entries()) { + const jobStates = jobs.map((job) => jobStateMap.get(job)!); + + // Determine status by priority: evaluating > error > queuing > completed + let status = EvaluationStatusEnum.queuing; + + if (jobStates.includes('active')) { + status = EvaluationStatusEnum.evaluating; + } else if (jobStates.includes('failed')) { + status = EvaluationStatusEnum.error; + } else if (jobStates.some((state) => ['waiting', 'delayed', 'prioritized'].includes(state))) { + status = EvaluationStatusEnum.queuing; + } else if (jobStates.includes('completed')) { + status = EvaluationStatusEnum.completed; + } + + statusMap.set(itemId, status); + } + } catch (error) { + addLog.error('Error getting batch evaluation item status:', { evalItemIds, error }); + // If error occurs, keep default status + } + + return statusMap; +} + +/** + * Get evaluation task statistics replacing database status field-based calculation + */ +export async function getEvaluationTaskStats(evalId: string): Promise<{ + total: number; + completed: number; + evaluating: number; + queuing: number; + error: number; +}> { + try { + // Get all evaluation items from database + const allEvalItems = await MongoEvalItem.find( + { evalId: new Types.ObjectId(evalId) }, + { _id: 1, finishTime: 1, errorMessage: 1 } + ).lean(); + + const totalItems = allEvalItems.length; + + if (totalItems === 0) { + return { + total: 0, + completed: 0, + evaluating: 0, + queuing: 0, + error: 0 + }; + } + + // Get related jobs from job queue + const itemJobs = await evaluationItemQueue.getJobs([ + 'waiting', + 'active', + 'delayed', + 'failed', + 'completed' + ]); + + const relatedJobs = itemJobs.filter((job) => job.data.evalId === evalId); + + // Create mapping from job ID to evaluation item ID + const jobsByItemId = new Map(); + relatedJobs.forEach((job) => { + if (job.data.evalItemId) { + jobsByItemId.set(job.data.evalItemId, job); + } + }); + + // Count status distribution + let completed = 0; + let evaluating = 0; + let queuing = 0; + let error = 0; + + // Optimize: batch get all job states to avoid multiple async calls in loop + const jobsToCheck = Array.from(jobsByItemId.values()); + const jobStatesWithJobs = await Promise.all( + jobsToCheck.map(async (job) => ({ + job, + state: await job.getState() + })) + ); + + // Create job to state mapping + const jobStateMap = new Map(); + jobStatesWithJobs.forEach(({ job, state }) => { + jobStateMap.set(job, state); + }); + + // Calculate status for each evaluation item (sync loop to avoid concurrent counter modification) + for (const item of allEvalItems) { + const itemId = item._id.toString(); + const job = jobsByItemId.get(itemId); + + if (job) { + // Has corresponding job, determine by job state + const jobState = jobStateMap.get(job); + + // Direct mapping from job state to evaluation state + if (jobState === 'active') { + evaluating++; + } else if (jobState === 'failed') { + error++; + } else if (jobState === 'completed') { + completed++; + } else if (['waiting', 'delayed', 'prioritized'].includes(jobState || '')) { + queuing++; + } else { + // Unknown job state, determine by database status + if (item.finishTime) { + if (item.errorMessage) { + error++; + } else { + completed++; + } + } else { + queuing++; + } + } + } else { + // No corresponding job, determine by database status + if (item.finishTime) { + if (item.errorMessage) { + error++; + } else { + completed++; + } + } else { + queuing++; + } + } + } + + const stats = { + total: totalItems, + completed, + evaluating, + queuing, + error + }; + + return stats; + } catch (error) { + addLog.error('Error getting evaluation task stats:', { evalId, error }); + return { + total: 0, + completed: 0, + evaluating: 0, + queuing: 0, + error: 0 + }; + } +} + +/** + * Check if evaluation task or item jobs are active + */ +export async function checkEvaluationTaskJobActive(evalId: string): Promise { + try { + const taskJobs = await evaluationTaskQueue.getJobs(['waiting', 'delayed', 'active']); + const itemJobs = await evaluationItemQueue.getJobs(['waiting', 'delayed', 'active']); + + const hasActiveTaskJob = taskJobs.some((job) => job.data.evalId === evalId); + const hasActiveItemJob = itemJobs.some((job) => job.data.evalId === evalId); + + return hasActiveTaskJob || hasActiveItemJob; + } catch (error) { + return false; + } +} + +/** + * Check if evaluation item job is active + */ +export async function checkEvaluationItemJobActive(evalItemId: string): Promise { + try { + const itemJobs = await evaluationItemQueue.getJobs(['waiting', 'delayed', 'active']); + return itemJobs.some((job) => job.data.evalItemId === evalItemId); + } catch (error) { + return false; + } +} diff --git a/packages/service/core/evaluation/utils/index.ts b/packages/service/core/evaluation/utils/index.ts index 001b91974609..321ba80899a4 100644 --- a/packages/service/core/evaluation/utils/index.ts +++ b/packages/service/core/evaluation/utils/index.ts @@ -6,51 +6,53 @@ import { EvaluationErrEnum } from '@fastgpt/global/common/error/code/evaluation' import { MAX_NAME_LENGTH, MAX_DESCRIPTION_LENGTH } from '@fastgpt/global/core/evaluation/constants'; import { MongoEvalDatasetCollection } from '../dataset/evalDatasetCollectionSchema'; import { Types } from 'mongoose'; +import { EvalMetricTypeEnum } from '@fastgpt/global/core/evaluation/metric/constants'; +import { getBuiltinMetrics } from '../metric/provider'; export type EvaluationValidationParams = Partial; export interface EvaluationValidationOptions { mode?: 'create' | 'update'; // validation mode - teamId?: string; // required for dataset existence validation + teamId?: string; // required for evalDatasetCollection existence validation } /** - * Validate if a dataset exists and is accessible by the team + * Validate if a evalDatasetCollection exists and is accessible by the team */ -async function validateDatasetExists( - datasetId: string, +async function validateEvalDatasetCollectionExists( + evalDatasetCollectionId: string, teamId?: string ): Promise { - // Validate datasetId format - if (!Types.ObjectId.isValid(datasetId)) { + // Validate evalDatasetCollectionId format + if (!Types.ObjectId.isValid(evalDatasetCollectionId)) { return { isValid: false, errors: [ { - code: EvaluationErrEnum.evalDatasetIdRequired, - message: 'Invalid dataset ID format', - field: 'datasetId' + code: EvaluationErrEnum.datasetCollectionIdRequired, + message: 'Invalid evalDatasetCollectionId format', + field: 'evalDatasetCollectionId' } ] }; } - // Check if dataset exists - const filter: any = { _id: new Types.ObjectId(datasetId) }; + // Check if evalDatasetCollection exists + const filter: any = { _id: new Types.ObjectId(evalDatasetCollectionId) }; if (teamId) { filter.teamId = new Types.ObjectId(teamId); } - const dataset = await MongoEvalDatasetCollection.findOne(filter).lean(); + const datasetCollection = await MongoEvalDatasetCollection.findOne(filter).lean(); - if (!dataset) { + if (!datasetCollection) { return { isValid: false, errors: [ { code: EvaluationErrEnum.datasetCollectionNotFound, - message: 'Dataset not found or access denied', - field: 'datasetId' + message: 'evalDatasetCollection not found or access denied', + field: 'evalDatasetCollectionId' } ] }; @@ -59,11 +61,52 @@ async function validateDatasetExists( return { isValid: true, errors: [] }; } +/** + * Validate builtin metric name + */ +async function validateBuiltinMetricName( + metricName: string, + metricType: string +): Promise { + if (metricType !== EvalMetricTypeEnum.Builtin) { + return { isValid: true, errors: [] }; + } + + try { + const builtinMetrics = await getBuiltinMetrics(); + const validMetricNames = builtinMetrics.map((metric) => metric.name); + + if (!validMetricNames.includes(metricName)) { + return { + isValid: false, + errors: [ + { + code: EvaluationErrEnum.evalMetricNameInvalid, + message: `Invalid builtin metric name '${metricName}'. Valid builtin metrics are: ${validMetricNames.join(', ')}`, + field: 'metric.name', + debugInfo: { + providedName: metricName, + validNames: validMetricNames + } + } + ] + }; + } + + return { isValid: true, errors: [] }; + } catch (err) { + // If we can't load builtin metrics, log error but allow validation to pass + // This prevents blocking evaluation creation if metric service is temporarily unavailable + console.warn('Failed to validate builtin metric name:', err); + return { isValid: true, errors: [] }; + } +} + export async function validateEvaluationParams( params: EvaluationValidationParams, options?: EvaluationValidationOptions ): Promise { - const { name, description, datasetId, target, evaluators } = params; + const { name, description, evalDatasetCollectionId, target, evaluators } = params; const mode = options?.mode || 'create'; const isCreateMode = mode === 'create'; @@ -82,14 +125,14 @@ export async function validateEvaluationParams( }; } - if (!datasetId) { + if (!evalDatasetCollectionId) { return { isValid: false, errors: [ { - code: EvaluationErrEnum.evalDatasetIdRequired, - message: 'Dataset ID is required', - field: 'datasetId' + code: EvaluationErrEnum.datasetCollectionIdRequired, + message: 'datasetCollectionId is required', + field: 'evalDatasetCollectionId' } ] }; @@ -166,22 +209,25 @@ export async function validateEvaluationParams( }; } - if (datasetId !== undefined) { - if (!datasetId) { + if (evalDatasetCollectionId !== undefined) { + if (!evalDatasetCollectionId) { return { isValid: false, errors: [ { - code: EvaluationErrEnum.evalDatasetIdRequired, - message: 'Dataset ID is required', - field: 'datasetId' + code: EvaluationErrEnum.datasetCollectionIdRequired, + message: 'datasetCollectionId is required', + field: 'evalDatasetCollectionId' } ] }; } - // Validate dataset exists and is accessible - const datasetValidation = await validateDatasetExists(datasetId, options?.teamId); + // Validate evaldatasetcollection exists and is accessible + const datasetValidation = await validateEvalDatasetCollectionExists( + evalDatasetCollectionId, + options?.teamId + ); if (!datasetValidation.isValid) { return datasetValidation; } @@ -242,6 +288,27 @@ export async function validateEvaluationParams( return { isValid: false, errors }; } + // Validate builtin metric name if type is builtin_metric + if (evaluator.metric) { + const builtinMetricValidation = await validateBuiltinMetricName( + evaluator.metric.name, + evaluator.metric.type + ); + if (!builtinMetricValidation.isValid) { + // Prefix error messages with evaluator index for clarity + const errors = builtinMetricValidation.errors.map((err) => ({ + ...err, + message: `Evaluator at index ${i}: ${err.message}`, + field: `evaluators[${i}].${err.field || 'unknown'}`, + debugInfo: { + evaluatorIndex: i, + ...err.debugInfo + } + })); + return { isValid: false, errors }; + } + } + // Validate scoreScaling if provided if (evaluator.scoreScaling !== undefined) { if ( diff --git a/packages/service/core/workflow/dispatch/dataset/search.ts b/packages/service/core/workflow/dispatch/dataset/search.ts index 575c99cbfcd7..f6e5f43d9b31 100644 --- a/packages/service/core/workflow/dispatch/dataset/search.ts +++ b/packages/service/core/workflow/dispatch/dataset/search.ts @@ -5,10 +5,16 @@ import { import { formatModelChars2Points } from '../../../../support/wallet/usage/utils'; import type { SelectedDatasetType } from '@fastgpt/global/core/workflow/type/io'; import type { SearchDataResponseItemType } from '@fastgpt/global/core/dataset/type'; -import type { SqlGenerationResponse , SearchDatasetDataResponse} from '../../../dataset/search/controller'; +import type { SearchDatasetDataResponse } from '../../../dataset/search/controller'; +import type { SqlGenerationResponse } from '@fastgpt/global/core/dataset/database/api'; import type { ModuleDispatchProps } from '@fastgpt/global/core/workflow/runtime/type'; import { getDefaultLLMModel, getEmbeddingModel, getRerankModel } from '../../../ai/model'; -import { deepRagSearch, defaultSearchDatasetData, SearchDatabaseData, generateAndExecuteSQL } from '../../../dataset/search/controller'; +import { + deepRagSearch, + defaultSearchDatasetData, + SearchDatabaseData, + generateAndExecuteSQL +} from '../../../dataset/search/controller'; import type { NodeInputKeyEnum, NodeOutputKeyEnum } from '@fastgpt/global/core/workflow/constants'; import { DispatchNodeResponseKeyEnum } from '@fastgpt/global/core/workflow/runtime/constants'; import { DatasetSearchModeEnum, DatasetTypeEnum } from '@fastgpt/global/core/dataset/constants'; @@ -45,7 +51,7 @@ type DatasetSearchProps = ModuleDispatchProps<{ [NodeInputKeyEnum.datasetDeepSearchModel]?: string; [NodeInputKeyEnum.datasetDeepSearchMaxTimes]?: number; [NodeInputKeyEnum.datasetDeepSearchBg]?: string; - + [NodeInputKeyEnum.generateSqlModel]?: string; }>; export type DatasetSearchResponse = DispatchNodeResultType<{ @@ -131,24 +137,16 @@ export async function dispatchDatasetSearch( // Check dataset types and separate them const datasetDetails = await Promise.all( - datasetIds.map(id => MongoDataset.findById(id, 'type databaseConfig').lean()) + datasetIds.map((id) => MongoDataset.findById(id, 'type databaseConfig').lean()) ); - const databaseDatasetIds = datasetIds.filter((_, index) => - datasetDetails[index]?.type === DatasetTypeEnum.database + const databaseDatasetIds = datasetIds.filter( + (_, index) => datasetDetails[index]?.type === DatasetTypeEnum.database ); - const commonDatasetIds = datasetIds.filter((_, index) => - datasetDetails[index]?.type !== DatasetTypeEnum.database + const commonDatasetIds = datasetIds.filter( + (_, index) => datasetDetails[index]?.type !== DatasetTypeEnum.database ); - addLog.info('Dataset Search - Dataset Type Separation', { - totalDatasets: datasetIds.length, - databaseDatasets: databaseDatasetIds.length, - commonDatasets: commonDatasetIds.length, - databaseDatasetIds, - commonDatasetIds - }); - // Results from different search types let commonSearchResult = null; let totalEmbeddingTokens = 0; @@ -167,93 +165,77 @@ export async function dispatchDatasetSearch( userChatInput: string, datasetId: string ): SearchDataResponseItemType => { - return { - id: `sql_result_${datasetId}`, - updateTime: new Date(), - q: userChatInput, // Use the original query as question - a: sqlResults.answer, // Use the generated answer as content - chunkIndex: 0, - datasetId: datasetId, - collectionId: `sql_collection_${datasetId}`, - sourceName: 'SQL Query Result', - sourceId: `sql_${datasetId}`, - score: [] // Empty score array as requested - }; - } + return { + id: `sql_result_${datasetId}`, + updateTime: new Date(), + q: userChatInput, // Use the original query as question + a: sqlResults.answer, // Use the generated answer as content + chunkIndex: 0, + datasetId: datasetId, + collectionId: `sql_collection_${datasetId}`, + sourceName: 'SQL Query Result', + sourceId: `sql_${datasetId}`, + score: [] // Empty score array as requested + }; + }; // Database search for database datasets - search each dataset individually and generate SQL if (databaseDatasetIds.length > 0) { - // if ((!generateSqlModel)) return getNodeErrResponse({error: new Error('no Generate-Sql Model Select')}); // Process each database dataset sequentially - await Promise.all(datasetIds.map(async (datasetId) => { - - const singleResult = await SearchDatabaseData({ - histories, - teamId, - queries: [userChatInput], - model: vectorModel.model, - limit, - datasetIds: [datasetId] - }); - if (singleResult) { - addLog.info('Dataset Search - Database Search Result', { - datasetId, - tokens: singleResult.tokens, - schemaTables: Object.keys(singleResult.schema).length + await Promise.all( + datasetIds.map(async (datasetId) => { + const singleResult = await SearchDatabaseData({ + histories, + teamId, + queries: [userChatInput], + model: vectorModel.model, + limit, + datasetIds: [datasetId] }); - totalEmbeddingTokens += singleResult.tokens; - if (Object.keys(singleResult.schema).length > 0) { - addLog.info('Dataset Search - Generating SQL', { - datasetId, - schemaTables: Object.keys(singleResult.schema), - query: userChatInput - }); - - const singleSqlResult = await generateAndExecuteSQL({ - datasetId, - query: userChatInput, - schema: singleResult.schema, - teamId, - limit, - generate_sql_llm: {model:(generateSqlModel ?? getDefaultLLMModel().name)}, - evaluate_sql_llm: {model:(generateSqlModel ?? getDefaultLLMModel().name)}, - }); - - if (singleSqlResult) { - addLog.info('Dataset Search - SQL Generation Success', { + if (singleResult) { + totalEmbeddingTokens += singleResult.tokens; + if (Object.keys(singleResult.schema).length > 0) { + const singleSqlResult = await generateAndExecuteSQL({ datasetId, - sql: singleSqlResult.sql.substring(0, 100) + '...', - dataCount: singleSqlResult.sql_res.data.length, - inputTokens: singleSqlResult.input_tokens, - outputTokens: singleSqlResult.output_tokens + query: userChatInput, + schema: singleResult.schema, + teamId, + limit, + generate_sql_llm: { model: generateSqlModel ?? getDefaultLLMModel().name }, + evaluate_sql_llm: { model: generateSqlModel ?? getDefaultLLMModel().name } }); - // Add to search results as chunks - searchRes.push(convertSqlResultsToChunks(singleSqlResult, userChatInput, datasetId)); - - // Collect for billing and response data - sqlResult.push({ - ...singleSqlResult, - datasetId - } as SqlResultWithDatasetId); + if (singleSqlResult) { + addLog.debug('Dataset Search - SQL Generation Success', { + datasetId, + sql: singleSqlResult.sql.substring(0, 100) + '...', + dataCount: singleSqlResult.sql_res.data.length, + inputTokens: singleSqlResult.input_tokens, + outputTokens: singleSqlResult.output_tokens + }); + + // Add to search results as chunks + searchRes.push( + convertSqlResultsToChunks(singleSqlResult, userChatInput, datasetId) + ); + + // Collect for billing and response data + sqlResult.push({ + ...singleSqlResult, + datasetId + } as SqlResultWithDatasetId); + } else { + addLog.warn('Dataset Search - SQL Generation Failed', { datasetId }); + } } else { - addLog.warn('Dataset Search - SQL Generation Failed', { datasetId }); + addLog.warn('Dataset Search - No schema found', { datasetId }); } } else { - addLog.warn('Dataset Search - No schema found', { datasetId }); + addLog.warn('Dataset Search - Database search failed', { datasetId }); } - } else { - addLog.warn('Dataset Search - Database search failed', { datasetId }); - } - })); + }) + ); } if (commonDatasetIds.length > 0) { - addLog.info('Dataset Search - Starting Common Dataset Search', { - commonDatasets: commonDatasetIds.length, - searchMode, - datasetDeepSearch, - usingReRank - }); - const searchData = { histories, teamId, @@ -284,13 +266,6 @@ export async function dispatchDatasetSearch( datasetSearchExtensionModel, datasetSearchExtensionBg }); - - addLog.info('Dataset Search - Common Search Completed', { - searchType: datasetDeepSearch ? 'deep' : 'default', - hasResults: !!commonSearchResult - }); - } else { - addLog.info('Dataset Search - No common datasets to search'); } embeddingTokens += totalEmbeddingTokens; @@ -312,14 +287,6 @@ export async function dispatchDatasetSearch( searchUsingReRank = commonResult.usingReRank; queryExtensionResult = commonResult.queryExtensionResult; deepSearchResult = commonResult.deepSearchResult; - - addLog.info('Dataset Search - Common Results Merged', { - commonResultsCount: commonResult.searchRes.length, - totalSearchResults: searchRes.length, - embeddingTokens: commonResult.embeddingTokens, - usingSimilarityFilter: commonResult.usingSimilarityFilter, - searchUsingReRank: commonResult.usingReRank - }); } // count bill results @@ -429,7 +396,7 @@ export async function dispatchDatasetSearch( }; })(); const totalPoints = nodeDispatchUsages.reduce((acc, item) => acc + item.totalPoints, 0); - + addLog.debug('Dataset Search - Final Statistics', { totalSearchResults: searchRes.length, totalPoints, @@ -456,17 +423,20 @@ export async function dispatchDatasetSearch( }), // SQL Result (for database datasets) - use first result or create summary ...(sqlResult.length > 0 && { - sqlResult: sqlResult.length === 1 ? { - sql: sqlResult[0].sql, - data: sqlResult[0].sql_res.data, - columns: sqlResult[0].sql_res.columns, - answer: sqlResult[0].answer - } : { - sql: sqlResult.map(r => r.sql).join('; '), - data: sqlResult.flatMap(r => r.sql_res.data), - columns: sqlResult[0].sql_res.columns, - answer: sqlResult.map(r => r.answer).join('\n\n') - } + sqlResult: + sqlResult.length === 1 + ? { + sql: sqlResult[0].sql, + data: sqlResult[0].sql_res.data, + columns: sqlResult[0].sql_res.columns, + answer: sqlResult[0].answer + } + : { + sql: sqlResult.map((r) => r.sql).join('; '), + data: sqlResult.flatMap((r) => r.sql_res.data), + columns: sqlResult[0].sql_res.columns, + answer: sqlResult.map((r) => r.answer).join('\n\n') + } }), searchUsingReRank, // Results diff --git a/packages/service/support/permission/evaluation/auth.ts b/packages/service/support/permission/evaluation/auth.ts index 7f234765db53..1b420ddc657f 100644 --- a/packages/service/support/permission/evaluation/auth.ts +++ b/packages/service/support/permission/evaluation/auth.ts @@ -19,8 +19,6 @@ 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 { EvaluationErrEnum } from '@fastgpt/global/common/error/code/evaluation'; -import { EvalMetricTypeEnum } from '@fastgpt/global/core/evaluation/metric/constants'; -import { TeamEvaluationCreatePermissionVal } from '@fastgpt/global/support/permission/user/constant'; // ================ Authentication and Authorization for eval task ================ export const authEvaluationByTmbId = async ({ @@ -302,7 +300,7 @@ export const authEvalMetricByTmbId = async ({ } // 团队权限验证 - 内置metric允许跨团队访问 - if (String(metric.teamId) !== teamId && metric.type !== EvalMetricTypeEnum.Builtin) { + if (String(metric.teamId) !== teamId) { return Promise.reject(EvaluationErrEnum.evalMetricNotFound); } @@ -315,11 +313,6 @@ export const authEvalMetricByTmbId = async ({ return { Per: new EvaluationPermission({ isOwner: true }) }; } - // 内置metric特殊处理:允许有evaluation权限的用户访问 - if (metric.type === EvalMetricTypeEnum.Builtin && String(metric.teamId) !== teamId) { - return { Per: new EvaluationPermission({ role: ReadPermissionVal, isOwner: false }) }; - } - // 获取evaluation资源的权限(evalMetric复用evaluation权限) const role = await getResourcePermission({ teamId, diff --git a/packages/service/type/env.d.ts b/packages/service/type/env.d.ts index 12ca3944f3f2..bfee3b10b03f 100644 --- a/packages/service/type/env.d.ts +++ b/packages/service/type/env.d.ts @@ -47,17 +47,6 @@ declare global { CHAT_LOG_SOURCE_ID_PREFIX?: string; NEXT_PUBLIC_BASE_URL: string; - - // evaluations settings - EVAL_TASK_CONCURRENCY?: string; - EVAL_ITEM_CONCURRENCY?: string; - EVAL_ITEM_MAX_RETRY?: string; - EVALUATION_DEFAULT_THRESHOLD?: string; - - // evalaution data settings - EVAL_DATA_QUALITY_CONCURRENCY?: string; - EVAL_DATASET_DATA_SYNTHESIZE_CONCURRENCY?: string; - EVAL_DATASET_SMART_GENERATE_CONCURRENCY?: string; } } } diff --git a/packages/web/components/common/Icon/constants.ts b/packages/web/components/common/Icon/constants.ts index f5953fa11074..34f967fcdda1 100644 --- a/packages/web/components/common/Icon/constants.ts +++ b/packages/web/components/common/Icon/constants.ts @@ -396,6 +396,7 @@ export const iconPaths = { 'file/qaImport': () => import('./icons/file/qaImport.svg'), 'file/uploadFile': () => import('./icons/file/uploadFile.svg'), fullScreen: () => import('./icons/fullScreen.svg'), + gradientLoading: () => import('./icons/gradientLoading.svg'), help: () => import('./icons/help.svg'), history: () => import('./icons/history.svg'), image: () => import('./icons/image.svg'), diff --git a/packages/web/components/common/Icon/icon.html b/packages/web/components/common/Icon/icon.html index 41fdac709ce0..42645d0d0de0 100644 --- a/packages/web/components/common/Icon/icon.html +++ b/packages/web/components/common/Icon/icon.html @@ -201,10 +201,10 @@ background-color: #4a90e2; color: white; border: none; - padding: 10px 20px; + padding: 8px 16px; border-radius: 4px; cursor: pointer; - font-size: 16px; + font-size: 14px; transition: background-color 0.3s; } @@ -231,7 +231,8 @@ .copy-success { position: fixed; top: 20px; - right: 20px; + left: 50%; + transform: translateX(-50%); background-color: #5cb85c; color: white; padding: 10px 20px; @@ -240,6 +241,17 @@ display: none; z-index: 1001; } + + /* 特殊处理白色填充的图标 */ + .icon-card[data-name="core/app/headphones"] .icon-container svg path, + .icon-card[data-name="core/chat/setting/share"] .icon-container svg path { + fill: black !important; + } + + /* 模态框中的特殊处理 */ + .modal-icon svg path[fill="white"] { + fill: black !important; + } @@ -278,7 +290,7 @@

FastGPT 图标展示

- + @@ -552,11 +564,11 @@

FastGPT 图标展示

} }); - // 复制路径按钮 + // 复制名称按钮 const copyButton = document.getElementById('copyButton'); copyButton.addEventListener('click', () => { - const path = document.getElementById('modalPath').textContent; - copyToClipboard(path); + const name = document.getElementById('modalName').textContent; + copyToClipboard(name); }); } diff --git a/packages/web/components/common/Icon/icons/gradientLoading.svg b/packages/web/components/common/Icon/icons/gradientLoading.svg new file mode 100644 index 000000000000..5408cd4b75f9 --- /dev/null +++ b/packages/web/components/common/Icon/icons/gradientLoading.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/web/components/common/MyTooltip/IconTip.tsx b/packages/web/components/common/MyTooltip/IconTip.tsx new file mode 100644 index 000000000000..5e584a53cc33 --- /dev/null +++ b/packages/web/components/common/MyTooltip/IconTip.tsx @@ -0,0 +1,21 @@ +import React from 'react'; +import MyTooltip from '.'; +import { type IconProps } from '@chakra-ui/icons'; +import MyIcon from '../Icon'; +import type { IconNameType } from '../Icon/type'; + +type Props = Omit & { + label?: string | React.ReactNode; + iconSrc: IconNameType; + maxW?: string | number; +}; + +const IconTip = ({ label, maxW, iconSrc, ...props }: Props) => { + return ( + + + + ); +}; + +export default React.memo(IconTip); diff --git a/packages/web/hooks/useScrollPagination.tsx b/packages/web/hooks/useScrollPagination.tsx index 708423b2cbad..76f80db81b8a 100644 --- a/packages/web/hooks/useScrollPagination.tsx +++ b/packages/web/hooks/useScrollPagination.tsx @@ -191,6 +191,7 @@ export function useScrollPagination< EmptyTip, showErrorToast = true, disabled = false, + pollingInterval, ...props }: { @@ -201,6 +202,7 @@ export function useScrollPagination< EmptyTip?: React.JSX.Element; showErrorToast?: boolean; disabled?: boolean; + pollingInterval?: number; } & Parameters[1] ) { const { t } = useTranslation(); @@ -209,23 +211,29 @@ export function useScrollPagination< const [data, setData] = useState([]); const [total, setTotal] = useState(0); const [isLoading, { setTrue, setFalse }] = useBoolean(false); - const isEmpty = total === 0 && !isLoading; + const isEmpty = total === 0 && !isLoading && data.length === 0; const noMore = data.length >= total; const loadData = useLockFn( async ({ init = false, - ScrollContainerRef + ScrollContainerRef, + silent = false }: { init?: boolean; ScrollContainerRef?: RefObject; + silent?: boolean; } = {}) => { if (noMore && !init) return; - setTrue(); + // 静默加载时不显示loading状态 + if (!silent) { + setTrue(); + } - if (init) { + // 静默加载时不清空现有数据,避免闪烁 + if (init && !silent) { setData([]); setTotal(0); } @@ -267,7 +275,8 @@ export function useScrollPagination< setData(newData); } } catch (error: any) { - if (showErrorToast) { + // 静默加载时不显示错误提示 + if (showErrorToast && !silent) { toast({ title: getErrText(error, t('common:core.chat.error.data_error')), status: 'error' @@ -276,7 +285,9 @@ export function useScrollPagination< console.log(error); } - setFalse(); + if (!silent) { + setFalse(); + } } ); @@ -358,10 +369,15 @@ export function useScrollPagination< useRequest2( async () => { if (disabled) return; - loadData({ init: true }); + // 有轮询间隔且已有数据时,使用静默加载 + const silent = !!pollingInterval && data.length > 0; + loadData({ init: true, silent }); }, { manual: false, + pollingInterval: pollingInterval, + pollingWhenHidden: false, + pollingErrorRetryCount: 0, ...props } ); diff --git a/packages/web/i18n/constants.ts b/packages/web/i18n/constants.ts index d568e47831ee..c58d4f30290e 100644 --- a/packages/web/i18n/constants.ts +++ b/packages/web/i18n/constants.ts @@ -20,7 +20,9 @@ export const I18N_NAMESPACES = [ 'account_team', 'account_model', 'dashboard_mcp', - 'dashboard_evaluation' + 'dashboard_evaluation', + 'evaluation', + 'admin' ]; export const I18N_NAMESPACES_MAP = I18N_NAMESPACES.reduce( diff --git a/packages/web/i18n/en/account.json b/packages/web/i18n/en/account.json index bb87544de36e..f1deb5bdfb90 100644 --- a/packages/web/i18n/en/account.json +++ b/packages/web/i18n/en/account.json @@ -2,22 +2,22 @@ "account_team.delete_dataset": "Delete knowledge base", "active_model": "Available models", "add_default_model": "Add preset model", - "api_key": "API key", - "bills_and_invoices": "Bills and invoices", + "api_key": "API keys", + "bills_and_invoices": "Bills and fapiaos", "channel": "Model channels", - "config_model": "Model configuration", + "config_model": "Model settings", "confirm_logout": "Are you sure you want to log out?", - "create_channel": "Add channel", - "create_model": "Add model", + "create_channel": "Add", + "create_model": "Add", "custom_model": "Custom model", "default_model": "Preset model", - "default_model_config": "Default model configuration", + "default_model_config": "Default model settings", "logout": "Log out", "model.active": "Enable", "model.alias": "Alias", "model.alias_tip": "Display name of the model in the system for easier understanding.", - "model.censor": "Sensitive content check", - "model.censor_tip": "Enable this option if sensitive content check is required.", + "model.censor": "Sensitive content detection", + "model.censor_tip": "Enable this if sensitive content detection is required.", "model.charsPointsPrice": "Overall price", "model.charsPointsPrice_tip": "Combine input and output for token billing. If input and output prices are configured separately, they will be calculated individually.", "model.custom_cq_prompt": "Custom prompt for question classification", @@ -25,62 +25,62 @@ "model.custom_extract_prompt": "Custom prompt for content extraction", "model.custom_extract_prompt_tip": "Overwrite the default system prompt. Default:\n\"\"\"\n{{prompt}}\n\"\"\"", "model.dataset_process": "Knowledge base file processing", - "model.defaultConfig": "Extra Body parameter", - "model.defaultConfig_tip": "The extra Body parameter will be included in every request.", - "model.default_config": "Extra Body field", + "model.defaultConfig": "Additional Body parameter", + "model.defaultConfig_tip": "The additional Body parameter will be included in each request.", + "model.default_config": "Additional Body field", "model.default_config_tip": "Merge these settings when users send a chat request. Example:\n\"\"\"\n{\n \"temperature\": 1,\n \"max_tokens\": null\n}\n\"\"\"", "model.default_model": "Default model", "model.default_system_chat_prompt": "Default prompt", - "model.default_system_chat_prompt_tip": "The default prompt will be included in all conversations with the model.", + "model.default_system_chat_prompt_tip": "The default prompt will be included in all chats using the model.", "model.default_token": "Default chunk size", - "model.default_token_tip": "Default text chunk size for indexing models. Must be smaller than the maximum context length.", + "model.default_token_tip": "Default text chunk size for indexing models, which must be smaller than the maximum context length.", "model.delete_model_confirm": "Are you sure you want to delete the model?", - "model.edit_model": "Edit model parameters", + "model.edit_model": "Edit model", "model.function_call": "Function calling", - "model.function_call_tip": "Enable this option if the model supports function calling. Tool calling takes higher priority.", + "model.function_call_tip": "Enable this if the model supports function calling. Tool calling takes higher priority.", "model.input_price": "Input price", - "model.input_price_tip": "Input price for the model. If configured, the overall price will become invalid.", + "model.input_price_tip": "If configured, the overall price will become invalid.", "model.json_config": "Configuration file", "model.json_config_confirm": "Are you sure you want to apply the configuration?", - "model.json_config_tip": "The configuration file will be used to overwrite the current configuration of the model. Please make sure the configuration file is correct and back up the current configuration first.", + "model.json_config_tip": "You can use the configuration file to overwrite the current model configuration. Please make sure the file is correct and back up the current configuration first.", "model.max_quote": "Max knowledge base references", "model.max_temperature": "Max temperature", "model.model_id": "Model ID", "model.model_id_tip": "Unique identifier of the model. This must match the model value from the provider and correspond to the OneAPI channel.", "model.normalization": "Normalization", - "model.normalization_tip": "If the Embedding API does not normalize vectors, enable this option to let the system normalize them.\nWithout normalization, the vector retrieval score will be greater than 1.", + "model.normalization_tip": "If the Embedding API does not normalize vectors, enable this option to let the system normalize them.\nWithout normalization, the vector search score will be greater than 1.", "model.output_price": "Output price", - "model.output_price_tip": "Output price for the model. If configured, the overall price will become invalid.", + "model.output_price_tip": "If configured, the overall price will become invalid.", "model.param_name": "Parameter name", "model.reasoning": "Reasoning output", "model.reasoning_tip": "For example, Deepseek-reasoner can output the reasoning process.", "model.request_auth": "Custom request key", - "model.request_auth_tip": "When you send requests to the custom request URL, include the header Authorization: Bearer xxx.", + "model.request_auth_tip": "TThe header Authorization: Bearer xxx will be included in the requests sent to the custom request URL.", "model.request_url": "Custom request URL", - "model.request_url_tip": "If configured, requests will be sent directly to this address without passing through OneAPI. Follow the OpenAI API format and provide a complete request URL. Example:\nLLM: {{host}}/v1/chat/completions\nEmbedding: {{host}}/v1/embeddings\nSTT: {{host}}/v1/audio/transcriptions\nTTS: {{host}}/v1/audio/speech\nRerank: {{host}}/v1/rerank", + "model.request_url_tip": "If specified, requests will be sent directly to this address without going through OneAPI. Follow the OpenAI API format and provide a complete request URL. Example:\nLLM: {{host}}/v1/chat/completions\nEmbedding: {{host}}/v1/embeddings\nSTT: {{host}}/v1/audio/transcriptions\nTTS: {{host}}/v1/audio/speech\nRerank: {{host}}/v1/rerank", "model.response_format": "Response format", "model.show_stop_sign": "Show stop sequence parameter", "model.show_top_p": "Show Top-p parameter", - "model.test_model": "Model test", - "model.tool_choice": "Enable tool calling", - "model.tool_choice_tag": "Tool calling", - "model.tool_choice_tip": "Enable this option if the model supports tool calling.", + "model.test_model": "Test model", + "model.tool_choice": "Tool calling", + "model.tool_choice_tag": "Tool call", + "model.tool_choice_tip": "Enable this if the model supports tool calling.", "model.used_in_classify": "Question classification", "model.used_in_extract_fields": "Text extraction", "model.used_in_query_extension": "Question optimization", "model.used_in_tool_call": "Tool calling node", "model.vision": "Image recognition", "model.vision_tag": "Vision", - "model.vision_tip": "Enable this option if the model supports image recognition.", + "model.vision_tip": "Enable this if the model supports image recognition.", "model.voices": "Voice role", "model.voices_tip": "Configure multiple roles using an array. Example:\n[\n {\n \"label\": \"Alloy\",\n \"value\": \"alloy\"\n },\n {\n \"label\": \"Echo\",\n \"value\": \"echo\"\n }\n]", - "model_provider": "Model provider", + "model_provider": "Models", "notifications": "Notification", - "personal_information": "Personal info", + "personal_information": "Profile", "personalization": "Personalization", - "promotion_records": "Promotion record", - "reset_default": "Reset to default", - "team": "Team management", + "promotion_records": "Promotion records", + "reset_default": "Restore defaults", + "team": "Teams", "third_party": "Third-party account", "usage_records": "Usage" } diff --git a/packages/web/i18n/en/account_bill.json b/packages/web/i18n/en/account_bill.json index 79c37a47f153..6cbbe4a904af 100644 --- a/packages/web/i18n/en/account_bill.json +++ b/packages/web/i18n/en/account_bill.json @@ -1,11 +1,11 @@ { - "Invoice_document": "Invoice file", + "Invoice_document": "Fapiao file", "all": "All", "back": "Back", "bank_account": "Account number", "bank_name": "Bank name", "bill_detail": "Bill details", - "bill_record": "Billing record", + "bill_record": "Billing records", "click_to_download": "Download", "company_address": "Company address", "company_phone": "Company phone", @@ -17,16 +17,16 @@ "default_header": "Default header", "detail": "Details", "email_address": "Email address", - "extra_ai_points": "Extra AI points", - "extra_dataset_size": "Extra knowledge base capacity", + "extra_ai_points": "Additional credits", + "extra_dataset_size": "Additional knowledge base indexes", "generation_time": "Time generated", "has_invoice": "Invoice issued", "invoice_amount": "Invoice amount", - "invoice_detail": "Invoice details", - "invoice_sending_info": "The invoice will be sent to the specified email address within 3-7 workdays. Please wait.", + "invoice_detail": "Fapiao details", + "invoice_sending_info": "The fapiao will be sent to the specified email address within 3-7 business days. Please wait.", "mm": "mm", "month": "Monthly", - "need_special_invoice": "VAT invoice required", + "need_special_invoice": "Special VAT fapiao required", "no": "No", "no_invoice_record": "No data available.", "no_invoice_record_tip": "No data available.", @@ -35,9 +35,10 @@ "organization_name": "Organization", "payment_method": "Payment method", "payway_coupon": "Redeem code", - "rerank": "Rerank", + "rerank": "Result reranking", + "generate_sql": "Generate SQL", "save": "Save", - "save_failed": "Error occurred during the operation.", + "save_failed": "Failed to save the settings.", "save_success": "Saved successfully.", "status": "Status", "sub_mode_custom": "Custom", @@ -49,8 +50,8 @@ "subscription_period": "Periodic", "support_wallet_amount": "Amount", "support_wallet_apply_invoice": "Invoiceable bill", - "support_wallet_bill_tag_invoice": "Bill invoice", - "support_wallet_invoicing": "Issue invoice", + "support_wallet_bill_tag_invoice": "Fapiao", + "support_wallet_invoicing": "Issue fapiao", "time": "Time", "total_amount": "Total amount", "type": "Type", diff --git a/packages/web/i18n/en/account_info.json b/packages/web/i18n/en/account_info.json index 955d06c2622a..689a6474cd1b 100644 --- a/packages/web/i18n/en/account_info.json +++ b/packages/web/i18n/en/account_info.json @@ -1,27 +1,27 @@ { "account_duplicate": "Account", - "account_knowledge_base_cleanup_warning": "If a team using the free edition is inactive for 30 days, its knowledge bases will be cleared automatically.", + "account_knowledge_base_cleanup_warning": "If a team using the free edition is inactive for 30 consecutive days, its knowledge bases will be cleared automatically.", "active": "Active", - "ai_points": "AI points", - "ai_points_calculation_standard": "AI points", - "ai_points_usage": "AI point usage", - "ai_points_usage_tip": "Each AI model call consumes AI points. For details, refer to the billing standard above.", + "ai_points": "Credits", + "ai_points_calculation_standard": "Credits", + "ai_points_usage": "Credit usage", + "ai_points_usage_tip": "Model calls consume credits. For details, refer to the billing standard above.", "app_amount": "Apps", "avatar": "Profile image", - "avatar_selection_exception": "Error occurred while selecting the profile image.", + "avatar_selection_exception": "Error occurred while uploading the profile image.", "balance": "Balance", "billing_standard": "Billing standard", "cancel": "Cancel", "change": "Change", - "choose_avatar": "Click to select a profile image.", - "click_modify_nickname": "Click to change nickname", - "code_required": "Verification code is required.", + "choose_avatar": "Click to upload a profile image.", + "click_modify_nickname": "Click to change the member name.", + "code_required": "Enter the verification code", "confirm": "OK", "confirm_password": "Confirm password", "contact_customer_service": "Contact Customer Service", - "contact_us": "Contact us", + "contact_us": "Contact Us", "current_package": "Current plan", - "current_token_price": "Current point price", + "current_token_price": "Current price for credit", "dataset_amount": "Knowledge bases", "effective_time": "Valid since", "email_label": "Email", @@ -33,9 +33,9 @@ "general_info": "Basics", "group": "groups", "help_chatbot": "Bot assistant", - "help_document": "Help documentation", - "knowledge_base_capacity": "Knowledge base capacity", - "manage": "Management", + "help_document": "Help", + "knowledge_base_capacity": "Knowledge base indexes", + "manage": "Manage", "member_amount": "Members", "member_name": "Member name", "month": "Monthly", @@ -44,39 +44,39 @@ "old_password": "Current password", "package_and_usage": "Plans and usage", "package_details": "Plan details", - "package_expiry_time": "Plan expiration time", + "package_expiry_time": "Expiration time", "package_usage_rules": "Plan usage rules: Higher-level plans are used first. Unused lower-level plans will be applied later.", "password": "Password", "password_mismatch": "Passwords do not match.", - "password_tip": "Password must be at least 8 characters long and contain at least 2 of the following: digits, letters, and special characters.", - "password_update_error": "Failed to change the password.", + "password_tip": "Must be at least 8 characters long and contain at least 2 of the following: digits, letters, and special characters.", + "password_update_error": "Error occurred while changing the password.", "password_update_success": "Password changed successfully.", "pending_usage": "Available", "phone_label": "Mobile number", - "please_bind_contact": "Please specify contact information.", - "please_bind_notification_receiving_path": "Please specify a notification recipient first.", - "purchase_extra_package": "Purchase extra plan", + "please_bind_contact": "Please specify a recipient.", + "please_bind_notification_receiving_path": "Please specify a recipient first.", + "purchase_extra_package": "Purchase additional plan", "redeem_coupon": "Redeem code", "reminder_create_bound_notification_account": "Remind the creator to specify an account to receive notifications.", "reset_password": "Reset password", "resource_usage": "Resource usage", - "select_avatar": "Click to select a profile image.", - "standard_package_and_extra_resource_package": "Includes the standard plan and extra resource packages.", + "select_avatar": "Click to upload a profile image.", + "standard_package_and_extra_resource_package": "Includes the standard plan and additional resource packages.", "storage_capacity": "Max shards", "team_balance": "Team balance", - "team_info": "Team info", - "token_validity_period": "Points are valid for 1 year.", - "tokens": "Points", + "team_info": "Team", + "token_validity_period": "Credits are valid for 1 year.", + "tokens": "Credits", "type": "Type", "unlimited": "Unlimited", "update_password": "Change password", - "update_success_tip": "Data updated successfully.", + "update_success_tip": "Updated successfully.", "upgrade_package": "Upgrade plan", "usage_balance": "Payment method: Balance", - "usage_balance_notice": "Due to a system upgrade, renewal with auto reduction from balance has been disabled, and balance top-up is no longer available. Your balance can still be used to purchase points.", + "usage_balance_notice": "Due to a system upgrade, renewal with auto reduction from balance has been disabled, and balance top-up is no longer available. Your balance can still be used to purchase credits.", "user_account": "Account", "user_team_team_name": "Team", - "verification_code": "Verification code", + "verification_code": "Code", "you_can_convert": "You can redeem", "yuan": "CNY." } diff --git a/packages/web/i18n/en/account_inform.json b/packages/web/i18n/en/account_inform.json index d35bf41d18cd..e09a4fef26b1 100644 --- a/packages/web/i18n/en/account_inform.json +++ b/packages/web/i18n/en/account_inform.json @@ -1,7 +1,7 @@ { - "notification_detail": "notification details", - "no_notifications": "No notification yet", + "notification_detail": "Notification details", + "no_notifications": "No data available.", "read": "Read", - "system": "official", - "team": "team" + "system": "Official", + "team": "Team" } \ No newline at end of file diff --git a/packages/web/i18n/en/account_model.json b/packages/web/i18n/en/account_model.json index 4fc5d2effbc3..1c98913d9cb6 100644 --- a/packages/web/i18n/en/account_model.json +++ b/packages/web/i18n/en/account_model.json @@ -1,95 +1,95 @@ { "Hunyuan": "Tencent Hunyuan", - "aipoint_usage": "AI points", + "aipoint_usage": "Credits consumed", "all": "All", "api_key": "API key", - "avg_response_time": "Average call time (seconds)", - "avg_ttfb": "Average first word duration (seconds)", - "azure": "Azure", - "base_url": "Base url", + "avg_response_time": "Avg call duration (s)", + "avg_ttfb": "Avg TTFB (s)", + "azure": "Microsoft Azure", + "base_url": "Proxy address", "batch_size": "Number of concurrent requests", - "channel_name": "Channel", + "channel_name": "Channel name", "channel_priority": "Priority", - "channel_priority_tip": "The higher the priority channel, the easier it is to be requested", - "channel_status": "state", - "channel_status_auto_disabled": "Automatically disable", + "channel_priority_tip": "Channels with higher priority are more likely to be used.", + "channel_status": "Status", + "channel_status_auto_disabled": "Auto disabled", "channel_status_disabled": "Disabled", - "channel_status_enabled": "Enable", - "channel_status_unknown": "unknown", - "channel_type": "Protocol Type", - "clear_model": "Clear the model", - "confirm_delete_channel": "Confirm the deletion of the [{{name}}] channel?", - "copy_model_id_success": "Copyed model id", - "create_channel": "Added channels", - "dashboard_call_trend": "Model Call Trend", + "channel_status_enabled": "Enabled", + "channel_status_unknown": "Unknown", + "channel_type": "Protocol type", + "clear_model": "Clear", + "confirm_delete_channel": "Are you sure you want to delete the channel ({{name}})?", + "copy_model_id_success": "Model ID copied successfully.", + "create_channel": "Add channel", + "dashboard_call_trend": "Model call trend", "dashboard_channel": "Channel", - "dashboard_cost_trend": "Cost Consumption", - "dashboard_error_calls": "Error Calls", - "dashboard_input_tokens": "Input Tokens", + "dashboard_cost_trend": "Credits consumed", + "dashboard_error_calls": "Failed calls", + "dashboard_input_tokens": "Input tokens", "dashboard_model": "Model", - "dashboard_no_data": "No data available", - "dashboard_output_tokens": "Output Tokens", - "dashboard_points": "points", - "dashboard_success_calls": "Success Calls", - "dashboard_token_trend": "Token Usage Trend", - "dashboard_token_usage": "Tokens", - "dashboard_total_calls": "Total Calls:", - "dashboard_total_cost": "Total Cost", - "dashboard_total_cost_label": "Total Cost:", - "dashboard_total_tokens": "Total Tokens", + "dashboard_no_data": "No data available.", + "dashboard_output_tokens": "Output tokens", + "dashboard_points": "Credits", + "dashboard_success_calls": "Successful calls", + "dashboard_token_trend": "Token usage trend", + "dashboard_token_usage": "Tokens consumed", + "dashboard_total_calls": "Total calls:", + "dashboard_total_cost": "Total cost", + "dashboard_total_cost_label": "Total cost:", + "dashboard_total_tokens": "Total tokens", "default_url": "Default address", - "detail": "Detail", - "duration": "Duration", - "edit": "edit", - "edit_channel": "Channel configuration", + "detail": "Details", + "duration": "Time taken", + "edit": "Edit", + "edit_channel": "Add channel", "enable_channel": "Enable", - "forbid_channel": "Disabled", + "forbid_channel": "Disable", "input": "Input", "key_type": "API key format:", - "log": "Call log", + "log": "Logs", "log_detail": "Log details", - "log_request_id_search": "Search by requestId", + "log_request_id_search": "Request ID", "log_status": "Status", - "mapping": "Model Mapping", - "mapping_tip": "A valid Json is required. \nThe model can be mapped when sending a request to the actual address. \nFor example:\n{\n \n \"gpt-4o\": \"gpt-4o-test\"\n\n}\n\nWhen FastGPT requests the gpt-4o model, the gpt-4o-test model is sent to the actual address, instead of gpt-4o.", - "maxToken_tip": "Model max_tokens parameter", - "max_rpm": "Max RPM (Requests Per Minute)", - "max_temperature_tip": "If the model temperature parameter is not filled in, it means that the model does not support the temperature parameter.", - "max_tpm": "Max TPM (Tokens Per Minute)", + "mapping": "Model mapping", + "mapping_tip": "Enter a valid JSON object to map the model to the actual URL. Example:\n{\n \"gpt-4o\": \"gpt-4o-test\"\n}\nWhen FastGPT requests the gpt-4o model, the system sends gpt-4o-test to the actual URL instead of gpt-4o.", + "maxToken_tip": "max_tokens parameter", + "max_rpm": "Max RPM", + "max_temperature_tip": "Model temperature parameter. Leave it blank if not supported.", + "max_tpm": "Max TPM", "model": "Model", - "model_error_rate": "Error rate", - "model_error_request_times": "Number of failures", + "model_error_rate": "Failure rate", + "model_error_request_times": "Failed calls", "model_name": "Model name", - "model_request_times": "Request times", - "model_test": "Model testing", - "model_tokens": "Input/Output tokens", - "model_ttfb_time": "Response time of first word", + "model_request_times": "Requests", + "model_test": "Test model", + "model_tokens": "Input/output tokens", + "model_ttfb_time": "TTFB", "monitoring": "Monitoring", "output": "Output", "request_at": "Request time", "request_duration": "Request duration: {{duration}}s", - "retry_times": "Number of retry times", - "running_test": "In testing", - "search_model": "Search for models", - "select_channel": "Select a channel name", - "select_model": "Select a model", - "select_model_placeholder": "Select the model available under this channel", - "select_provider_placeholder": "Search protocol type", - "selected_model_empty": "Choose at least one model", - "start_test": "Batch test {{num}} models", - "test_failed": "There are {{num}} models that report errors", - "timespan_day": "Day", - "timespan_hour": "Hour", - "timespan_label": "Time Granularity", - "timespan_minute": "Minute", - "total_call_volume": "Request amount", - "use_in_eval": "Use in eval", + "retry_times": "Max attempts", + "running_test": "Testing", + "search_model": "Model ID", + "select_channel": "Select", + "select_model": "Select", + "select_model_placeholder": "Select models", + "select_provider_placeholder": "Protocol type", + "selected_model_empty": "Please select at least one model.", + "start_test": "Bulk test", + "test_failed": "{{num}} models encountered error.", + "timespan_day": "Daily", + "timespan_hour": "Hourly", + "timespan_label": "Time granularity", + "timespan_minute": "Minutes", + "total_call_volume": "Total calls", + "use_in_eval": "App evaluation", "view_chart": "Chart", - "view_table": "Table", - "vlm_model": "Vlm", - "vlm_model_tip": "Used to generate additional indexing of images in a document in the knowledge base", - "volunme_of_failed_calls": "Error amount", - "waiting_test": "Waiting for testing", - "evaluation_model": "评测模型", - "evaluation_model_tip": "用于应用评测,及评测数据集中针对数据质量的评测。" + "view_table": "Form", + "vlm_model": "VLM", + "vlm_model_tip": "Generates additional indexes for images in documents within the knowledge base.", + "volunme_of_failed_calls": "Failed calls", + "waiting_test": "Waiting for test", + "evaluation_model": "Model", + "evaluation_model_tip": "Used to evaluate apps and data quality evaluation in datasets." } diff --git a/packages/web/i18n/en/account_promotion.json b/packages/web/i18n/en/account_promotion.json index b3d40b3cd936..8ca70b73a88d 100644 --- a/packages/web/i18n/en/account_promotion.json +++ b/packages/web/i18n/en/account_promotion.json @@ -1,13 +1,13 @@ { "amount": "Amount", "cashback_ratio": "Cashback rate", - "cashback_ratio_description": "You will receive a balance reward proportionate to your friend's top-up amount.", + "cashback_ratio_description": "You will receive a balance reward based on your friend's top-up amount.", "copy_invite_link": "Copy invitation link", "earnings": "Income (¥)", "invite_url": "Invitation link", "invite_url_tip": "Friends who register through this link will be permanently linked to your account. Each time they top up, you will receive a balance reward.\nIf they register with a mobile number, you will get an immediate 5 CNY bonus.\nAll rewards are credited to your default team account.", "no_invite_records": "No data available.", "time": "Time", - "total_invited": "Total invites", + "total_invited": "Invitees", "type": "Type" } \ No newline at end of file diff --git a/packages/web/i18n/en/account_setting.json b/packages/web/i18n/en/account_setting.json index 142f701c89ce..751b6c1c57a7 100644 --- a/packages/web/i18n/en/account_setting.json +++ b/packages/web/i18n/en/account_setting.json @@ -2,5 +2,5 @@ "language": "Language", "personalization": "Personalization", "timezone": "Time zone", - "update_data_success": "Data updated successfully." + "update_data_success": "Updated successfully." } \ No newline at end of file diff --git a/packages/web/i18n/en/account_team.json b/packages/web/i18n/en/account_team.json index 5613ae0df43b..e9db3a7edba1 100644 --- a/packages/web/i18n/en/account_team.json +++ b/packages/web/i18n/en/account_team.json @@ -1,5 +1,5 @@ { - "1person": "1 person", + "1person": "1", "1year": "1 year", "30mins": "30 mins", "7days": "7 days", @@ -15,8 +15,8 @@ "admin_delete_plugin": "Delete plugin", "admin_delete_plugin_group": "Delete plugin group", "admin_delete_template_type": "Delete template category", - "admin_finish_invoice": "Issue invoice", - "admin_login": "Admin login", + "admin_finish_invoice": "Issue fapiao", + "admin_login": "Admin logs in", "admin_save_template_type": "Update template category", "admin_send_system_inform": "Send system notification", "admin_update_app_template": "Update template", @@ -25,16 +25,16 @@ "admin_update_plugin_group": "Update plugin group", "admin_update_system_config": "Update system configuration", "admin_update_system_modal": "Configure system announcement", - "admin_update_team": "Edit team info", - "admin_update_user": "Edit user info", - "assign_permission": "Change permission", + "admin_update_team": "Edit team", + "admin_update_user": "Edit user", + "assign_permission": "Change permissions", "audit_log": "Audit", "change_department_name": "Edit department", "change_member_name": "Change member name", "change_member_name_self": "Change member name", "change_notification_settings": "Change notification recipient", "change_password": "Change password", - "confirm_delete_from_org": "Are you sure you want to remove {{username}} from the department?", + "confirm_delete_from_org": "Are you sure you want to remove the member ({{username}}) from the department?", "confirm_delete_from_team": "Are you sure you want to remove the member ({{username}}) from the team?", "confirm_delete_group": "Are you sure you want to delete the group?", "confirm_delete_org": "Are you sure you want to delete the department?", @@ -43,21 +43,21 @@ "copy_link": "Copy link", "create_api_key": "Create API key", "create_app": "Create app", - "create_app_copy": "Create app replica", + "create_app_copy": "Duplicate app", "create_app_folder": "Create app folder", - "create_app_publish_channel": "Create sharing channel", + "create_app_publish_channel": "Create publishing channel", "create_collection": "Create collection", "create_data": "Insert data", "create_dataset": "Create knowledge base", "create_dataset_folder": "Create knowledge base folder", "create_department": "Create sub-department", "create_evaluation_dataset_collection": "Create evaluation dataset collection", - "create_evaluation_dataset_data": "Create evaluation dataset data", + "create_evaluation_dataset_data": "Create evaluation data", "create_evaluation_task": "Create evaluation task", - "create_evaluation_metric": "Create evaluation metric", + "create_evaluation_metric": "Create evaluation metrics", "create_group": "Create group", "create_invitation_link": "Create invitation link", - "create_invoice": "Issue invoice", + "create_invoice": "Issue fapiao", "create_org": "Create department", "create_sub_org": "Create sub-department", "dataset.api_file": "API import", @@ -66,61 +66,61 @@ "dataset.feishu_dataset": "Feishu bitable", "dataset.folder_dataset": "Folder", "dataset.website_dataset": "Website sync", - "dataset.yuque_dataset": "Yuque knowledge base", + "dataset.yuque_dataset": "Yuque", "delete": "Delete", "delete_api_key": "Delete API key", - "delete_app": "Delete workspace app", - "delete_app_collaborator": "Remove app permission", + "delete_app": "Delete app", + "delete_app_collaborator": "Delete app permissions", "delete_app_publish_channel": "Delete publishing channel", "delete_collection": "Delete collection", "delete_data": "Delete data", "delete_dataset": "Delete knowledge base", - "delete_dataset_collaborator": "Remove knowledge base permission", + "delete_dataset_collaborator": "Delete knowledge base permissions", "delete_department": "Delete sub-department", "delete_evaluation": "Delete app evaluation data", "delete_evaluation_dataset_collection": "Delete evaluation dataset collection", - "delete_evaluation_dataset_data": "Delete evaluation dataset data", + "delete_evaluation_dataset_data": "Delete evaluation data", "delete_evaluation_dataset_task": "Delete evaluation dataset task", "delete_evaluation_task": "Delete evaluation task", "delete_evaluation_task_item": "Delete evaluation task item", - "delete_evaluation_task_data_item": "Delete evaluation task data item", + "delete_evaluation_task_data_item": "Delete evaluation data item", "update_evaluation_task_data_item": "Update evaluation task data item", "retry_evaluation_task_data_item": "Retry evaluation task data item", "export_evaluation_task_data_items": "Export evaluation task data items", - "delete_evaluation_metric": "Delete evaluation metric", - "delete_from_org": "Remove user from department", - "delete_from_team": "Remove user from team", + "delete_evaluation_metric": "Delete evaluation metrics", + "delete_from_org": "Remove member from department", + "delete_from_team": "Remove member from team", "delete_group": "Delete group", "delete_org": "Delete department", "department": "Department", "edit_info": "Edit", "edit_member": "Edit user", "edit_member_tip": "Member name", - "edit_org_info": "Edit department info", + "edit_org_info": "Edit department", "expires": "Expiration time", - "export_app_chat_log": "Export app chat history", - "export_bill_records": "Export billing record", + "export_app_chat_log": "Export chat history", + "export_bill_records": "Export billing records", "export_dataset": "Export knowledge base", - "export_evaluation_task_items": "Export evaluation task items", - "generate_evaluation_summary": "Generate evaluation summary report", - "update_evaluation_summary_config": "Update evaluation summary config", + "export_evaluation_task_items": "Export evaluation task item", + "generate_evaluation_summary": "Generate summary report", + "update_evaluation_summary_config": "Update summary settings", "export_members": "Export member", - "forbid_hint": "Disabled invitation links will become invalid and cannot be restored. Are you sure you want to disable this invitation link?", + "forbid_hint": "This operation will invalidate the invitation link, and cannot be undone. Would you like to proceed?", "forbid_success": "Disabled successfully.", "forbidden": "Disable", "group": "Group", "group_name": "Group name", - "handle_invitation": "Manage team invitations", + "handle_invitation": "Team invitations", "has_forbidden": "Expired", - "import_evaluation_dataset_data": "Import evaluation dataset data", + "import_evaluation_dataset_data": "Import evaluation data", "has_invited": "Invited", "ignore": "Ignored", - "inform_level_common": "Moderate", - "inform_level_emergency": "Critical", - "inform_level_important": "Important", - "invitation_copy_link": "[{{systemName}}] {{userName}} has invited you to join the team ({{teamName}}). Click the following link to join: {{url}}", + "inform_level_common": "Low", + "inform_level_emergency": "High", + "inform_level_important": "Medium", + "invitation_copy_link": "[{{systemName}}] {{userName}} invited you to join the team ({{teamName}}). Click the following link to join: {{url}}", "invitation_link_auto_clean_hint": "Expired links will be automatically cleared 30 days later.", - "invitation_link_description": "Link description", + "invitation_link_description": "Description", "invitation_link_list": "Links", "invite_member": "Invite member", "invited": "Invited", @@ -129,7 +129,7 @@ "kick_out_team": "Remove member", "label_sync": "Sync tag", "leave": "Resigned", - "leave_team_failed": "Failed to leave the team.", + "leave_team_failed": "Error occurred while leaving the team.", "log_admin_add_plan": "{{name}} added a plan for the team ({{teamId}}).", "log_admin_add_user": "{{name}} created the user ({{userName}}).", "log_admin_change_license": "{{name}} changed the license.", @@ -140,110 +140,110 @@ "log_admin_delete_plugin": "{{name}} deleted the plugin ({{pluginName}}).", "log_admin_delete_plugin_group": "{{name}} deleted the plugin group ({{groupName}}).", "log_admin_delete_template_type": "{{name}} deleted the template category ({{typeName}}).", - "log_admin_finish_invoice": "{{name}} issued an invoice for the team ({{teamName}}).", + "log_admin_finish_invoice": "{{name}} issued a fapiao for the team ({{teamName}}).", "log_admin_login": "{{name}} logged in to the admin platform.", "log_admin_save_template_type": "{{name}} added the template category ({{typeName}}).", - "log_admin_send_system_inform": "{{name}} sent a system notification titled {{informTitle}} with priority {{level}}.", + "log_admin_send_system_inform": "{{name}} sent a system notification titled {{informTitle}} with the {{level}} level.", "log_admin_update_app_template": "{{name}} updated the template ({{templateName}}).", - "log_admin_update_plan": "{{name}} edited plan information for the team ({{teamId}}).", + "log_admin_update_plan": "{{name}} edited plan information for the team (ID: {{teamId}}).", "log_admin_update_plugin": "{{name}} updated the plugin ({{pluginName}}).", "log_admin_update_plugin_group": "{{name}} updated the plugin group ({{groupName}}).", "log_admin_update_system_config": "{{name}} updated system configuration.", "log_admin_update_system_modal": "{{name}} configured a system announcement.", "log_admin_update_team": "{{name}} edited the information (Name: {{newTeamName}}, Balance: {{newBalance}}) of the team ({{teamName}}).", - "log_admin_update_user": "Information of the user ({{userName}}) was modified.", - "log_assign_permission": "{{name}} updated the permissions (App creation: {{appCreate}}, Knowledge base: {{datasetCreate}}, API key: {{apiKeyCreate}}, and Management: {{manage}}) of {{objectName}}.", + "log_admin_update_user": "Edited the user ({{userName}}).", + "log_assign_permission": "{{name}} updated the {{objectName}} permissions (App creation: {{appCreate}}, Knowledge base creation: {{datasetCreate}}, API key creation: {{apiKeyCreate}}, and Administrator {{manage}}).", "log_change_department": "{{name}} updated the department ({{departmentName}}).", - "log_change_member_name": "{{name}} renamed changed the name of the member from {{memberName}} to {{newName}}.", - "log_change_member_name_self": "{{name}} changed their own name from {{oldName}} to {{newName}}.", + "log_change_member_name": "{{name}} changed the member name from {{memberName}} to {{newName}}.", + "log_change_member_name_self": "{{name}} changed his/her own name from {{oldName}} to {{newName}}.", "log_change_notification_settings": "{{name}} changed the notification recipient.", "log_change_password": "{{name}} changed the password.", "log_create_api_key": "{{name}} created the API key ({{keyName}}).", - "log_create_app": "{{name}} created the {{appType}} app ({{appName}}).", - "log_create_app_copy": "{{name}} created a replica for the {{appType}} app ({{appName}}).", + "log_create_app": "{{name}} created the {{appType}} ({{appName}}).", + "log_create_app_copy": "{{name}} duplicated the {{appType}} ({{appName}}).", "log_create_app_folder": "{{name}} created the folder ({{folderName}}).", - "log_create_app_publish_channel": "{{name}} created the channel ({{channelName}}) for the {{appType}} app ({{appName}}).", - "log_create_collection": "{{name}} created the collection ({{collectionName}}) in the {{datasetType}} named {{datasetName}}.", - "log_create_data": "{{name}} inserted data into the collection named {{collectionName}} in the {{datasetType}} named {{datasetName}}.", - "log_create_dataset": "{{name}} deleted the {{datasetType}} named {{datasetName}}.", + "log_create_app_publish_channel": "{{name}} created the channel ({{channelName}}) for the {{appType}} ({{appName}}).", + "log_create_collection": "{{name}} created the collection ({{collectionName}}) in the {{datasetType}} ({{datasetName}}).", + "log_create_data": "{{name}} inserted data into the collection ({{collectionName}}) in the {{datasetType}} ({{datasetName}}).", + "log_create_dataset": "{{name}} create the {{datasetType}}({{datasetName}}).", "log_create_dataset_folder": "{{name}} created the folder ({{folderName}}).", "log_create_department": "{{name}} created the department ({{departmentName}}).", - "log_create_evaluation_dataset_collection": "【{{name}}】Created evaluation dataset collection named [{{collectionName}}]", - "log_create_evaluation_dataset_data": "【{{name}}】Created evaluation dataset data in collection [{{collectionName}}]", - "log_create_evaluation_task": "【{{name}}】Created evaluation task named [{{taskName}}]", - "log_create_evaluation_metric": "【{{name}}】Created evaluation metric [{{metricName}}]", + "log_create_evaluation_dataset_collection": "{{name}} created the evaluation dataset ({{collectionName}}).", + "log_create_evaluation_dataset_data": "{{name}} created evaluation data in the evaluation dataset ({{collectionName}}).", + "log_create_evaluation_task": "{{name}} created the evaluation task ({{taskName}}).", + "log_create_evaluation_metric": "{{name}} created the evaluation metric ({{metricName}}).", "log_create_group": "{{name}} created the group ({{groupName}}).", "log_create_invitation_link": "{{name}} created the invitation link ({{link}}).", - "log_create_invoice": "{{name}} issued an invoice.", + "log_create_invoice": "{{name}} issued a fapiao.", "log_delete_api_key": "{{name}} deleted the API key ({{keyName}}).", - "log_delete_app": "{{name}} deleted the {{appType}} app ({{appName}}).", - "log_delete_app_collaborator": "{{name}} deleted the {{itemName}} permission ({{itemValueName}}) in the {{appType}} app ({{appName}}).", - "log_delete_app_publish_channel": "{{name}} deleted the channel ({{channelName}}) from the {{appType}} app ({{appName}}).", - "log_delete_collection": "{{name}} deleted the collection ({{collectionName}}) from the {{datasetType}} named {{datasetName}}.", - "log_delete_data": "{{name}} deleted data from the collection named {{collectionName}} in the {{datasetType}} named {{datasetName}}.", - "log_delete_dataset": "{{name}} deleted the {{datasetType}} named {{datasetName}}.", - "log_delete_dataset_collaborator": "{{name}} deleted the {{itemName}} permission ({{itemValueName}}) from the {{datasetType}} named {{datasetName}}.", + "log_delete_app": "{{name}} deleted the {{appType}} ({{appName}}).", + "log_delete_app_collaborator": "{{name}} deleted the {{itemName}} (({{itemValueName}})) permission on the {{appType}} ({{appName}}).", + "log_delete_app_publish_channel": "{{name}} deleted the channel ({{channelName}}) for the {{appType}} ({{appName}}).", + "log_delete_collection": "{{name}} deleted the collection ({{collectionName}}) from the {{datasetType}} ({{datasetName}}).", + "log_delete_data": "{{name}} deleted data from the collection ({{collectionName}}) in the {{datasetType}} ({{datasetName}}).", + "log_delete_dataset": "{{name}} deleted the {{datasetType}}({{datasetName}}).", + "log_delete_dataset_collaborator": "{{name}} deleted the {{itemName}} ({{itemValueName}}) permission on the {{datasetType}} ({{datasetName}}).", "log_delete_department": "{{name}} deleted the department ({{departmentName}}).", - "log_delete_evaluation": "{{name}} deleted evaluation data for the {{appType}} app ({{appName}}).", + "log_delete_evaluation": "{{name}} deleted evaluation data of the {{appType}} ({{appName}}).", "log_delete_group": "{{name}} deleted the group ({{groupName}}).", - "log_delete_evaluation_dataset_collection": "【{{name}}】Deleted evaluation dataset collection named [{{collectionName}}]", - "log_delete_evaluation_dataset_data": "【{{name}}】Deleted evaluation dataset data in collection [{{collectionName}}]", - "log_delete_evaluation_dataset_task": "【{{name}}】Deleted evaluation dataset task in collection [{{collectionName}}]", - "log_delete_evaluation_task": "【{{name}}】Deleted evaluation task named [{{taskName}}]", - "log_delete_evaluation_task_item": "【{{name}}】Deleted evaluation task item [{{itemId}}] from task [{{taskName}}]", - "log_delete_evaluation_metric": "【{{name}}】Deleted evaluation metric [{{metricName}}]", + "log_delete_evaluation_dataset_collection": "{{name}} deleted the evaluation dataset ({{collectionName}}).", + "log_delete_evaluation_dataset_data": "{{name}} deleted evaluation data in the evaluation dataset ({{collectionName}}).", + "log_delete_evaluation_dataset_task": "{{name}} deleted a task in the evaluation dataset ({{collectionName}}).", + "log_delete_evaluation_task": "{{name}} deleted the evaluation task ({{taskName}}).", + "log_delete_evaluation_task_item": "{{name}} deleted the item ({{itemId}}) in the evaluation task ({{taskName}}).", + "log_delete_evaluation_metric": "{{name}} deleted the evaluation metric ({{metricName}}).", "log_details": "Details", - "log_export_app_chat_log": "{{name}} exported the chat history of the {{appType}} app ({{appName}}).", + "log_export_app_chat_log": "{{name}} exported the chat history of the {{appType}} ({{appName}}).", "log_export_bill_records": "{{name}} exported the billing records.", - "log_export_dataset": "{{name}} exported the {{datasetType}} named {{datasetName}}.", - "log_generate_evaluation_summary": "{{name}} generated summary report for metric {{metricName}} in evaluation task {{evalName}}.", - "log_update_evaluation_summary_config": "{{name}} updated summary configuration for evaluation task {{evalName}}.", - "log_import_evaluation_dataset_data": "【{{name}}】Imported {{recordCount}} evaluation dataset records into collection [{{collectionName}}]", - "log_export_evaluation_task_items": "【{{name}}】Exported evaluation task items from task [{{taskName}}]", - "log_delete_evaluation_task_data_item": "【{{name}}】Deleted data item [{{dataItemId}}] from evaluation task [{{taskName}}]", - "log_update_evaluation_task_data_item": "【{{name}}】Updated data item [{{dataItemId}}] in evaluation task [{{taskName}}]", - "log_retry_evaluation_task_data_item": "【{{name}}】Retried data item [{{dataItemId}}] in evaluation task [{{taskName}}]", - "log_export_evaluation_task_data_items": "【{{name}}】Exported {{itemCount}} data items from evaluation task [{{taskName}}] in {{format}} format", - "log_join_team": "{{name}} joined the team using the invitation link ({{link}}).", + "log_export_dataset": "{{name}} exported the {{datasetType}}({{datasetName}}).", + "log_generate_evaluation_summary": "{{name}} generated a summary report for the metric ({{metricName}}) in the evaluation task ({{evalName}}).", + "log_update_evaluation_summary_config": "{{name}} updated the summary settings of the evaluation task ({{evalName}}).", + "log_import_evaluation_dataset_data": "{{name}} imported {{recordCount}} entries into the evaluation dataset ({{collectionName}}).", + "log_export_evaluation_task_items": "{{name}} exported {{itemCount}} items from the evaluation task ({{taskName}}) in {{format}} format.", + "log_delete_evaluation_task_data_item": "{{name}} deleted the item ({{dataItemId}}) in the evaluation task ({{taskName}}).", + "log_update_evaluation_task_data_item": "{{name}} updated the item ({{dataItemId}}) in the evaluation task ({{taskName}}).", + "log_retry_evaluation_task_data_item": "{{name}} retried the item ({{dataItemId}}) in the evaluation task ({{taskName}}).", + "log_export_evaluation_task_data_items": "{{name}} exported {{itemCount}} items from the evaluation task ({{taskName}}) in {{format}} format.", + "log_join_team": "{{name}} joined the team via the invitation link ({{link}}).", "log_kick_out_team": "{{name}} removed the member ({{memberName}}).", "log_login": "{{name}} logged into the system.", - "log_move_app": "{{name}} moved the {{appType}} app ({{appName}}) to the folder ({{targetFolderName}}).", - "log_move_dataset": "{{name}} moved the {{datasetType}} named {{datasetName}} to the folder ({{targetFolderName}}).", + "log_move_app": "{{name}} moved the {{appType}} ({{appName}}) to the folder ({{targetFolderName}}).", + "log_move_dataset": "{{name}} moved the {{datasetType}}({{datasetName}}) to the folder ({{targetFolderName}}).", "log_purchase_plan": "{{name}} purchased a plan.", - "log_quality_assessment_evaluation_data": "【{{name}}】Performed quality assessment on evaluation data in collection [{{collectionName}}]", + "log_quality_assessment_evaluation_data": "{{name}} evaluated data quality in the evaluation dataset ({{collectionName}}).", "log_recover_team_member": "{{name}} restored the member ({{memberName}}).", "log_relocate_department": "{{name}} moved the department ({{departmentName}}).", - "log_retrain_collection": "{{name}} retrained the collection named {{collectionName}} in the {{datasetType}} named {{datasetName}}.", - "log_retry_evaluation_dataset_task": "【{{name}}】Retried evaluation dataset task in collection [{{collectionName}}]", - "log_retry_evaluation_task": "【{{name}}】Retried failed items in evaluation task [{{taskName}}]", - "log_retry_evaluation_task_item": "【{{name}}】Retried evaluation task item [{{itemId}}] from task [{{taskName}}]", - "log_search_test": "{{name}} performed a search test in the {{datasetType}} named {{datasetName}}.", - "log_smart_generate_evaluation_data": "【{{name}}】Smart generated evaluation data in collection [{{collectionName}}]", - "log_set_invoice_header": "{{name}} set the invoice header.", + "log_retrain_collection": "{{name}} retrained the collection({{collectionName}}) in the {{datasetType}} ({{datasetName}}).", + "log_retry_evaluation_dataset_task": "{{name}} retried a task in the evaluation dataset ({{collectionName}}).", + "log_retry_evaluation_task": "{{name}} retried the evaluation task ({{taskName}}) with {{retryCount}} items retried.", + "log_retry_evaluation_task_item": "{{name}} retried the item ({{itemId}}) in the evaluation task ({{taskName}}).", + "log_search_test": "{{name}} performed a search test for the {{datasetType}} ({{datasetName}}).", + "log_smart_generate_evaluation_data": "{{name}} auto-generated evaluation data in the evaluation dataset ({{collectionName}}).", + "log_set_invoice_header": "{{name}} set the fapiao title.", "log_time": "Time", - "log_transfer_app_ownership": "{{name}} transferred ownership of the {{appType}} app ({{appName}}) from {{oldOwnerName}} to {{newOwnerName}}.", - "log_transfer_dataset_ownership": "{{name}} transferred ownership of the {{datasetType}} named {{datasetName}} from {{oldOwnerName}} to {{newOwnerName}}.", + "log_transfer_app_ownership": "{{name}} transferred ownership of the {{appType}} ({{appName}}) from {{oldOwnerName}} to {{newOwnerName}}.", + "log_transfer_dataset_ownership": "{{name}} transferred ownership of the {{datasetType}} ({{datasetName}}) from {{oldOwnerName}} to {{newOwnerName}}.", "log_type": "Operation", "log_update_api_key": "{{name}} updated the API key ({{keyName}}).", - "log_update_app_collaborator": "{{name}} updated the collaborators (Organizations: {{orgList}}, Groups: {{groupList}}, Members: {{tmbList}}) and permissions (Read: {{readPermission}}, Write: {{writePermission}}, Admin: {{managePermission}}) of the {{appType}} app ({{appName}}).", - "log_update_app_info": "{{name}} updated the {{appType}} app ({{appName}}): {{newItemNames}} set to {{newItemValues}}.", - "log_update_app_publish_channel": "{{name}} updated the channel ({{channelName}}) for the {{appType}} app ({{appName}}).", - "log_update_collection": "{{name}} updated the collection {{collectionName}} in the {{datasetType}} named {{datasetName}}.", - "log_update_data": "{{name}} updated data in the collection {{collectionName}} in the {{datasetType}} named {{datasetName}}.", - "log_update_dataset": "{{name}} updated the {{datasetType}} named {{datasetName}}.", - "log_update_dataset_collaborator": "{{name}} updated the collaborators (Organizations: {{orgList}}, Groups: {{groupList}}, Members: {{tmbList}}) and permissions ({{readPermission}}, {{writePermission}}, {{managePermission}}) of the {{datasetType}} named {{datasetName}}.", - "log_update_evaluation_dataset_collection": "【{{name}}】Updated evaluation dataset collection named [{{collectionName}}]", - "log_update_evaluation_dataset_data": "【{{name}}】Updated evaluation dataset data in collection [{{collectionName}}]", - "log_update_evaluation_task": "【{{name}}】Updated evaluation task named [{{taskName}}]", - "log_update_evaluation_task_item": "【{{name}}】Updated evaluation task item [{{itemId}}] from task [{{taskName}}]", - "log_start_evaluation_task": "【{{name}}】Started evaluation task named [{{taskName}}]", - "log_stop_evaluation_task": "【{{name}}】Stopped evaluation task named [{{taskName}}]", - "log_update_evaluation_metric": "【{{name}}】Updated evaluation metric [{{metricName}}]", - "log_debug_evaluation_metric": "【{{name}}】 debugged evaluation metric [{{metricName}}]", - "log_update_publish_app": "{{name}} performed the operation ({{operationName}}) on the {{appType}} app ({{appName}}).", + "log_update_app_collaborator": "{{name}} updated the {{appType}} ({{appName}}) collaborator (Department: {{orgList}}, Group: {{groupList}}, Member: {{tmbList}}) permissions (Read: {{readPermission}}, Write: {{writePermission}}, Admin: {{managePermission}}).", + "log_update_app_info": "{{name}} updated the {{newItemNames}} of the {{appType}} ({{appName}}) to {{newItemValues}}.", + "log_update_app_publish_channel": "{{name}} updated the channel ({{channelName}}) for the {{appType}} ({{appName}}).", + "log_update_collection": "{{name}} updated the collection {{collectionName}} in the {{datasetType}} ({{datasetName}}).", + "log_update_data": "{{name}} updated data in the collection {{collectionName}} in the {{datasetType}} ({{datasetName}}).", + "log_update_dataset": "{{name}} updated the {{datasetType}}({{datasetName}}).", + "log_update_dataset_collaborator": "{{name}} updated the {{datasetType}} ({{datasetName}}) collaborator (Department: {{orgList}}, Group: {{groupList}}, Member: {{tmbList}}) permissions ({{readPermission}}, {{writePermission}}, {{managePermission}}).", + "log_update_evaluation_dataset_collection": "{{name}} updated the evaluation dataset ({{collectionName}}).", + "log_update_evaluation_dataset_data": "{{name}} updated evaluation data in the evaluation dataset ({{collectionName}}).", + "log_update_evaluation_task": "{{name}} updated the evaluation task ({{taskName}}).", + "log_update_evaluation_task_item": "{{name}} updated the item ({{itemId}}) in the evaluation task ({{taskName}}).", + "log_start_evaluation_task": "{{name}} started the evaluation task ({{taskName}}).", + "log_stop_evaluation_task": "{{name}} stopped the evaluation task ({{taskName}}).", + "log_update_evaluation_metric": "{{name}} updated the evaluation metric ({{metricName}}).", + "log_debug_evaluation_metric": "{{name}} debugged the evaluation metric ({{metricName}}).", + "log_update_publish_app": "{{name}} performed the operation ({{operationName}}) on the {{appType}} ({{appName}}).", "log_user": "Operator", "login": "Log in", - "manage_member": "Manage member", + "manage_member": "Members", "member": "Member", "member_group": "Group", "move_app": "Move app", @@ -255,40 +255,39 @@ "org_description": "Description", "org_name": "Department name", "owner": "Owner", - "permission": "Permission", + "permission": "Permissions", "permission_apikeyCreate": "Create API key", "permission_apikeyCreate_Tip": "Create global API keys and MCP services.", "permission_appCreate": "Create app", - "permission_appCreate_tip": "Create apps in the root directory. (Permissions within folders are controlled by the folder.)", + "permission_appCreate_tip": "Create apps in the root directory but not in folders.", "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_datasetCreate_Tip": "Create knowledge bases in the root directory but not in folders", "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.", + "permission_evaluationCreate_Tip": "You can create evaluation tasks, metrics, and datasets.", + "permission_manage_tip": "Manage members, create and manage groups, and assign permissions to groups and members.", + "please_bind_contact": "Please specify a recipient.", "purchase_plan": "Upgrade plan", - "quality_assessment_evaluation_data": "Quality assessment evaluation data", + "quality_assessment_evaluation_data": "Quality evaluation data", "recover_team_member": "Restore member", "relocate_department": "Move department", "retry_evaluation_dataset_task": "Retry evaluation dataset task", "retry_evaluation_task": "Retry evaluation task", "retry_evaluation_task_item": "Retry evaluation task item", "remark": "Remarks", - "remove_tip": "Are you sure you want to remove the member ({{username}}) from the team? The member will be marked as Left, their operation data will not be deleted, and resources under their account will be automatically transferred to the team owner.", - "restore_tip": "Are you sure you want to add the member ({{username}}) to the team? The member's account and related permissions will be restored, but the account resources will not be recovered.", + "remove_tip": "Are you sure you want to remove the member ({{username}}) from the team? The member will be marked as Left, their operation data will not be deleted, but account resources will be automatically transferred to the team owner.", + "restore_tip": "Are you sure you want to add the member ({{username}}) to the team? The member's account and related permissions will be restored, but the account resources cannot be recovered.", "restore_tip_title": "Confirm", "retain_admin_permissions": "Retain admin permissions", "retrain_collection": "Retrain collection", - "save_and_publish": "Save and publish", + "save_and_publish": "Save & publish", "search_log": "Log", "search_member": "Member", "search_member_group_name": "Member name, group name", "search_org": "Department", "search_test": "Test", - "set_invoice_header": "Set invoice title", + "set_invoice_header": "Set fapiao title", "set_name_avatar": "Team avatar & name", - "smart_generate_evaluation_data": "Smart generate evaluation data", + "smart_generate_evaluation_data": "Auto generate evaluation data", "sync_immediately": "Sync now", "sync_member_failed": "Failed to sync members.", "sync_member_success": "Members synced successfully.", @@ -296,36 +295,41 @@ "transfer_app_ownership": "Transfer app ownership", "transfer_dataset_ownership": "Transfer knowledge base ownership", "transfer_ownership": "Transfer ownership", - "type.Folder": "Folder", + "type.Folder": "folder", "type.Http plugin": "HTTP plugin", - "type.Plugin": "Plugin", - "type.Simple bot": "Simple app", - "type.Tool": "Tool", + "type.Plugin": "plugin", + "type.Simple bot": "simple app", + "type.Tool": "tool", "type.Tool set": "toolkit", - "type.Workflow bot": "Workflow", + "type.Workflow bot": "workflow", "unlimited": "Unlimited", "update": "Update", "update_api_key": "Update API key", - "update_app_collaborator": "Change app permission", - "update_app_info": "Edit app info", + "update_app_collaborator": "Change app permissions", + "update_app_info": "Edit app", "update_app_publish_channel": "Update publishing channel", "update_collection": "Update collection", "update_data": "Update data", "update_dataset": "Update knowledge base", - "update_dataset_collaborator": "Change Knowledge base permission", + "update_dataset_collaborator": "Change knowledge base permissions", "update_evaluation_dataset_collection": "Update evaluation dataset collection", - "update_evaluation_dataset_data": "Update evaluation dataset data", + "update_evaluation_dataset_data": "Update evaluation data", "update_evaluation_task": "Update evaluation task", "update_evaluation_task_item": "Update evaluation task item", "start_evaluation_task": "Start evaluation task", "stop_evaluation_task": "Stop evaluation task", - "update_evaluation_metric": "Update evaluation metric", - "debug_evaluation_metric": "Debug evaluation metric", - "update_publish_app": "App update", - "used_times_limit": "Active users", + "update_evaluation_metric": "Update evaluation metrics", + "debug_evaluation_metric": "Debug evaluation metrics", + "update_publish_app": "Update app", + "used_times_limit": "Invitee limit", "user_name": "Username", "user_team_invite_member": "Invite member", "user_team_leave_team": "Leave team", "user_team_leave_team_failed": "Failed to leave the team.", - "waiting": "Pending" -} + "waiting": "Pending", + "create_evaluation": "创建应用评测", + "export_evaluation": "导出应用评测数据", + "log_create_evaluation": "【{{name}}】创建了名为【{{appName}}】的【{{appType}}】的批量评测", + "log_export_evaluation": "【{{name}}】导出了名为【{{appName}}】的【{{appType}}】的评测数据", + "permission_evaluationCreate": "创建评估" +} \ No newline at end of file diff --git a/packages/web/i18n/en/account_thirdParty.json b/packages/web/i18n/en/account_thirdParty.json index f9bd1a1529db..89cad6f2e748 100644 --- a/packages/web/i18n/en/account_thirdParty.json +++ b/packages/web/i18n/en/account_thirdParty.json @@ -5,11 +5,11 @@ "laf_account": "LAF account", "no_intro": "No data available.", "not_configured": "Not configured", - "open_api_notice": "You can enter an OpenAI/OneAPI key. The key will be used for AI Conversation, Question Classification, and Content Extraction without extra charges. Make sure the key can be used to access the required models. You can choose FastAI as the GPT model.", + "open_api_notice": "Enter an OpenAI/OneAPI key. The key will be used for AI chats, question classification, and content extraction without charges. Make sure the key can be used to access the corresponding models. You can choose FastAI as the GPT model.", "openai_account_configuration": "OpenAI/OneAPI account", - "openai_account_setting_exception": "Failed to set OpenAI account.", - "request_address_notice": "Request address. Default: official OpenAI address. You can enter a proxy address. Note that \"/v1\" will not be automatically appended to the address.", - "third_party_account": "Third-party account", + "openai_account_setting_exception": "Error occurred while configuring the OpenAI account.", + "request_address_notice": "Request address. Default: official OpenAI address You can enter a proxy address. Note that \"/v1\" will not be automatically appended to the address.", + "third_party_account": "Third-party accounts", "third_party_account.configured": "Configured", "third_party_account.not_configured": "Not configured", "third_party_account_desc": "The admin can configure third-party accounts or variables, and these accounts will be available to all team members.", diff --git a/packages/web/i18n/en/account_usage.json b/packages/web/i18n/en/account_usage.json index bd676f7bf751..e6fc6bab70f8 100644 --- a/packages/web/i18n/en/account_usage.json +++ b/packages/web/i18n/en/account_usage.json @@ -1,62 +1,63 @@ { - "ai_model": "AI model", - "all": "all", - "app_name": "Application name", - "auto_index": "Auto index", - "billing_module": "Deduction module", - "confirm_export": "A total of {{total}} pieces of data were filtered out. Are you sure to export?", - "count": "Number of runs", - "current_filter_conditions": "Current filter conditions", + "ai_model": "Model", + "all": "All", + "app_name": "App name", + "auto_index": "Index enhancement", + "billing_module": "Billing details", + "confirm_export": "Are you sure you want to export the {{total}} matched entries?", + "count": "Runs", + "current_filter_conditions": "Current filters:", "dashboard": "Dashboard", "details": "Details", "dingtalk": "DingTalk", - "duration_seconds": "Duration (seconds)", - "embedding_index": "Embedding", - "evaluation": "Application Review", - "evaluation_dataset_data_quality_assessment": "Evaluation Dataset Data Quality Assessment", - "evaluation_dataset_data_synthesis": "Evaluation Dataset Synthesis", - "evaluation_dataset_data_qa_synthesis": "Evaluation Q&A Synthesis", - "evaluation_quality_assessment": "Quality Assessment", - "evaluation_debug_metric": "Debug Metric", - "every_day": "Day", - "every_month": "Moon", + "duration_seconds": "Duration (s)", + "embedding_index": "Index generation", + "evaluation": "App evaluation", + "evaluation_dataset_data_quality_assessment": "Data quality evaluation", + "evaluation_dataset_data_synthesis": "Evaluation data synthesis", + "evaluation_dataset_data_qa_synthesis": "Evaluation data Q&A synthesis", + "evaluation_quality_assessment": "Quality evaluation", + "evaluation_debug_metric": "Debug metrics", + "every_day": "days", + "every_month": "months", "every_week": "weekly", - "export_confirm": "Export confirmation", - "export_confirm_tip": "There are currently {{total}} usage records in total. Are you sure to export?", - "export_success": "Export successfully", - "export_title": "Time,Members,Type,Project name,AI points", + "export_confirm": "Confirm", + "export_confirm_tip": "Are you sure you want to export {{total}} entries?", + "export_success": "Export successful.", + "export_title": "Time, Member, Type, Project name, Credits consumed", "feishu": "Feishu", - "generation_time": "Generation time", + "generation_time": "Time generated", "image_index": "Image index", - "image_parse": "Image tagging", - "input_token_length": "input tokens", - "llm_paragraph": "LLM segmentation", + "image_parse": "Image mark", + "input_token_length": "Input tokens", + "llm_paragraph": "Text segmentation", "mcp": "MCP call", - "member": "member", - "metrics_execute": "Metrics Execute", + "member": "Member", + "metrics_execute": "Execute metrics", "member_name": "Member name", - "module_name": "module name", - "month": "moon", - "no_usage_records": "No usage record yet", - "official_account": "Official Account", - "order_number": "Order number", - "output_token_length": "output tokens", + "module_name": "Module name", + "month": "months", + "no_usage_records": "No data available.", + "official_account": "WeChat official account", + "order_number": "Order ID", + "output_token_length": "Output tokens", "pages": "Pages", - "pdf_enhanced_parse": "PDF Enhanced Analysis", - "pdf_parse": "PDF Analysis", - "points": "Points", + "pdf_enhanced_parse": "Enhanced PDF parsing", + "pdf_parse": "PDF parsing", + "points": "Credits", "project_name": "Project name", - "qa": "QA", + "qa": "Q&A pair extraction", "select_member_and_source_first": "Please select members and types first", - "share": "Share Link", - "source": "source", - "text_length": "text length", - "token_length": "token length", - "total_points": "AI points consumption", - "total_points_consumed": "AI points consumption", - "total_usage": "Total Usage", - "usage_detail": "Details", - "user_type": "type", + "share": "Sharing link", + "source": "Source", + "text_length": "Text length", + "token_length": "Token length", + "total_points": "Credits consumed", + "total_points_consumed": "Credits consumed", + "total_usage": "Total consumption", + "usage_detail": "Usage details", + "user_type": "Type", "wecom": "WeCom", - "evaluation_summary_generation": "Evaluation - Summary Generation" -} + "evaluation_summary_generation": "Generate summary", + "generate_answer": "生成应用回答" +} \ No newline at end of file diff --git a/packages/web/i18n/en/admin.json b/packages/web/i18n/en/admin.json new file mode 100644 index 000000000000..205e5ba437f1 --- /dev/null +++ b/packages/web/i18n/en/admin.json @@ -0,0 +1,624 @@ +{ + "license_active_success": "Activated successfully.", + "system_activation": "System activation", + "system_activation_desc": "You need to activate FastGPT first.", + "domain_name": "Current domain", + "input_license": "Enter the license key", + "cancel": "Cancel", + "confirm": "OK", + "config_desc": "Configuration description", + "domain_invalid": "The license domain is invalid.", + "change_license": "Change license", + "logout": "Logout", + "expire_time": "Expiration date", + "max_users": "Max users", + "max_apps": "Max apps", + "max_datasets": "Max knowledge bases", + "sso": "SSO", + "pay": "Payment system", + "custom_templates": "Custom templates and system tools", + "dataset_enhance": "Knowledge base enhancement", + "unlimited": "Unlimited", + "data_dashboard": "Dashboard", + "notification_management": "Notification management", + "log_management": "Log management", + "user_management": "User management", + "user_info": "Users", + "team_management": "Team management", + "plan_management": "Subscription plans", + "payment_records": "Payment record", + "invoice_management": "Invoices", + "resource_management": "Resources", + "app_management": "App management", + "dataset_management": "Knowledge base management", + "system_config": "System", + "basic_config": "General", + "feature_list": "Features", + "security_review": "Security audit", + "third_party_providers": "Third-party accounts", + "user_config": "User settings", + "plan_recharge": "Plans & top-up", + "template_tools": "Templates & tools", + "template_market": "Templates", + "toolbox": "Toolbox", + "audit_logs": "Audit logs", + "first_page": "First page", + "previous_page": "Back", + "next_page": "Next", + "last_page": "Last page", + "page": "Page", + "click_view_details": "View details", + "upload_image_failed": "Upload image failed", + "config_file": "Configuration file", + "save": "Save", + "upload_profile_failed": "Upload avatar failed", + "associated_plugin_is_empty": "Associated plugin cannot be empty", + "config_success": "Configuration successful.", + "confirm_delete_plugin": "Are you sure you want to delete the plugin?", + "delete_success": "Deleted successfully.", + "custom_plugin": "Custom plugin", + "config": "Settings", + "give_name": "Name", + "click_upload_avatar": "Upload tool image", + "app_name_empty": "App name is required.", + "description": "Description", + "add_app_description": "Add description for this app", + "associated_plugins": "Associated plugins", + "search_plugin": "Plugin name, app ID", + "attribute": "Attribute", + "author_name": "Author name", + "default_system_name": "System name is used by default", + "is_enable": "Enable", + "charge_token_fee": "Charge tokens", + "call_price": "API call cost (n points/call)", + "instructions": "User guide", + "use_markdown_syntax": "Use Markdown syntax.", + "update": "Update", + "create_plugin": "Add plugin", + "delete_confirm_message": "Related resources will be deleted and cannot be restored. Would you like to proceed?", + "group_management": "Groups", + "total_groups": "Total groups: {localGroups.length}", + "add": "Add", + "add_type": "Add type", + "avatar_select_error": "Error occurred while selecting the tool image.", + "rename": "Rename", + "add_group": "Add group", + "avatar_name": "Tool image & name", + "click_set_avatar": "Upload tool image", + "group_name_empty": "Group name is required.", + "official": "Official", + "configured": "Configured", + "not_configured": "Not configured", + "system_key_price": "System key price (n points/time)", + "plugin_config": "{{name}} configuration", + "enable_toolset": "Enable toolset", + "config_system_key": "Configure system key", + "tool_list": "Tool list", + "tool_name": "Tool name", + "key_price": "Key price", + "continue": "Continue", + "user_deleted_message": "This account has been deleted.", + "missing_field": "Required fields are missing.", + "team_not_exist": "The team does not exist.", + "subscription_exists": "A subscription of the same type already exists.", + "subscription_not_exist": "The subscription does not exist.", + "user_exist": "The user already exists.", + "account_logout": "Account logged out.", + "user_not_found": "User not found.", + "user_not_exist": "The user does not exist.", + "update_failed": "Update failed.", + "send_notification_success": "Notification sent successfully.", + "user_password_error": "User or password error!", + "invoice_not_found": "Invoice not found.", + "invoice_issued": "Invoice has been issued.", + "invoice_completed_notice": "The invoice you requested has been issued. Please check.", + "invoice_generation_completed": "Invoice issued successfully.", + "order_not_exist": "The order does not exist.", + "order_unpaid": "The order is unpaid.", + "order_invoiced_no_refund": "Refund is unavailable because the order has already been invoiced.", + "order_insufficient_amount": "The order amount is insufficient.", + "type_empty": "Type cannot be empty.", + "quantity_must_be_positive": "Quantity must be greater than 0.", + "team_not_found": "Team cannot be found.", + "dataset_training_rebuilding": "The dataset is being trained or rebuilt. Please try again later.", + "database_rebuilding_index": "Rebuild database index", + "type_not_support": "This message type is not supported.", + "send_verification_code_success": "Verification code sent successfully.", + "normal": "Normal", + "leave": "Leave", + "deactivated": "Disabled", + "account": "Account", + "username": "Username", + "contact": "Contact information", + "department": "Department", + "join_time": "Time added", + "update_time": "Last updated", + "status": "Status", + "team_num_limit_error": "A user can only join to one team.", + "organization_name": "Organization", + "amount": "Amount", + "yuan": "CNY", + "pending_invoice_count": "Pending Invoice Count", + "go_to_view": "View now", + "new_invoice_application": "New invoice request received.", + "order_type_error": "The order type is invalid.", + "missing_key_params_update_bill_failed": "Failed to update the bill because required parameters are missing. Please contact the administrator.", + "plugin_service_link_not_configured": "Plugin service link not configured", + "audit_record_table": "Audit logs", + "operator": "Operator", + "operation_type": "Operation", + "operation_time": "Time", + "operation_content": "Description", + "no_audit_records": "No data available.", + "audit_details": "Audit details", + "details": "Details", + "close": "Disable", + "total_users": "Total Users", + "registered_users": "Registered Users", + "payment_amount": "Total amount", + "order_count": "Orders", + "all": "All", + "success": "Successful", + "paid_teams": "Paid teams", + "total_conversations": "Total chats", + "total_sessions": "Total sessions", + "avg_conversations_per_session": "Average chats per session", + "points_consumed": "Points consumed", + "user_total": "Total users", + "dataset_total": "Total knowledge bases", + "app_total": "Total apps", + "statistics_data": "Statistics", + "traffic": "Traffic", + "payment": "Payment", + "active": "Active", + "cost": "Cost", + "last_7_days": "Last 7 days", + "last_30_days": "Last 30 days", + "last_90_days": "Last 90 days", + "last_180_days": "Last 180 days", + "confirm_modify_system_announcement": "Are you sure you want to change the system announcement?", + "confirm_send_system_notification": "Are you sure you want to send the system notification?", + "modify_success": "Modified successfully.", + "modify_failed": "Modification failed.", + "send_success": "Sent successfully.", + "send_failed": "Operation failed.", + "system_announcement_config": "System announcement", + "system_announcement_description": "The announcement will pop up after users log in to FastGPT, and will not appear again after being closed. You can configure only 1 system announcement. Supported Markdown format.", + "send_system_notification": "System notification", + "confirm_send": "Send now", + "send_notification_description": "Send a notification to all users. Notification prompts vary by level.", + "message_level": "Message level", + "level_normal": "Low (platform system message only)", + "level_important": "Medium (platform system message + login notification)", + "level_urgent": "High (platform system message + login notification + email/SMS alert)", + "notification_title": "Subject", + "notification_content": "Content", + "log_record_table": "Log Record Table", + "log_search_placeholder": "Enter log content to search, press Enter to search", + "time": "Time", + "log_content": "Log Content", + "no_log_records": "No data available.", + "log_level": "Log severity", + "log_detail": "Log details", + "log_message": "Log Message", + "login_success": "Login successful.", + "admin_login": "Admin login", + "login_username": "Username", + "login_password": "Password", + "login_button": "Log in", + "app_list": "Apps", + "app_name": "App Name", + "creator": "Creator", + "redirect": "Redirect", + "app_no_records": "No data available.", + "app_details": "App details", + "app_id": "App ID", + "app_creator_id": "Creator ID", + "dataset_list": "Knowledge bases", + "dataset_name": "Knowledge base name", + "dataset_data_size": "Data Size", + "dataset_vector_count": "Total vectors", + "dataset_favorite_count": "Favorites", + "new": "New", + "name": "Name", + "enable": "Enable", + "redirect_link": "Redirect URL", + "operation": "Operation", + "add_sidebar_item": "Add sidebar item", + "sidebar_item_name": "Sidebar item name", + "sidebar_item_name_empty": "Sidebar item name is required.", + "redirect_link_empty": "Redirect URL is required.", + "edit_plan": "Edit {{label}} plan", + "plan_name": "Plan Name", + "custom_plan_name_desc": "Custom plan name (overwrites the default name)", + "monthly_price": "Monthly price", + "max_team_members": "Max team members", + "max_app_count": "Max apps", + "max_dataset_count": "Max Dataset Count", + "history_retention_days": "History retention (days)", + "max_dataset_index_count": "Max knowledge base indexes", + "monthly_ai_points": "Monthly AI points", + "training_priority_high": "Training priority (higher value for higher priority)", + "allow_site_sync": "Enable site sync", + "allow_team_operation_logs": "Enable team operation logs", + "click_configure_plan": "Configure plan", + "copy_success": "Copied successfully.", + "delete_confirm": "Are you sure you want to delete the variable?", + "delete": "Delete", + "workflow_variable_custom": "Custom workflow variables", + "field_name": "Variable name", + "usage_url": "Usage query URL", + "note": "Note", + "import_config": "Import config file", + "import_success_save": "Import successful. Please click Save to save the changes.", + "import_check_format": "Please check the configuration file format.", + "import": "Import", + "get_config_error": "Error occurred while loading configuration.", + "save_success": "Saved successfully.", + "save_failed": "Sending failed.", + "frontend_display_config": "Configure frontend view", + "personalization_config": "Personalization", + "global_script": "Global script", + "system_params": "System parameters", + "pdf_parse_config": "PDF parsing settings", + "usage_limits": "Usage limit", + "sidebar_config": "Sidebar settings", + "system_name": "System name", + "custom_api_domain": "Custom API domain", + "custom_api_domain_desc": "Set an additional API address. If you do not use the address of the main site, you need to configure the domain’s CNAME and SSL certificate.", + "custom_share_link_domain": "Custom sharing link domain", + "custom_share_link_domain_desc": "Set an additional sharing link. If you do not use the address of the main site, you need to configure the domain's CNAME and SSL certificate.", + "openapi_prefix": "OpenAPI prefix", + "contact_popup": "Contact Us pop-up", + "contact_popup_desc": "Configure the content for Contact us using Markdown syntax.", + "custom_api_doc_url": "Custom API documentation URL", + "custom_openapi_doc_url": "Custom OpenAPI documentation URL", + "doc_url_note": "Documentation URL (It must end with a slash (/). Otherwise, users cannot be redirected to the correct subpaths.)", + "contribute_plugin_doc_url": "Link for plugin contribution", + "contribute_template_doc_url": "Link for template contribution", + "global_script_desc": "Custom script (inserted globally for site traffic monitoring or other purposes)", + "mcp_forward_service_url": "MCP forwarding service address", + "mcp_forward_service_desc": "Deploy an MCP forwarding service to expose FastGPT via the MCP protocol. Example: http://localhost:3005", + "oneapi_url": "OneAPI address (overwrites environment variable configuration)", + "oneapi_url_desc": "OneAPI address (overwrites environment variable configuration)", + "input_oneapi_url": "Used for accessing multiple models", + "oneapi_key": "OneAPI key (overwrites environment variable configuration)", + "input_oneapi_key": "Enter the OneAPI address", + "dataset_parse_max_process": "Max parsing processes for knowledge base", + "dataset_index_max_process": "Max indexing processes for knowledge base", + "file_understanding_max_process": "Max processes for LLM", + "image_understanding_max_process": "Max processes for VLM", + "hnsw_ef_search": "HNSW ef_search", + "hnsw_ef_search_desc": "HNSW parameter. A higher value improves recall but reduces performance. Default value: 100. For details, see https://github.com/pgvector/pgvector", + "hnsw_max_scan_tuples": "HNSW max_scan_tuples", + "hnsw_max_scan_tuples_desc": "Maximum iterations. A higher value improves recall but reduces performance. Default value: 100000. For details, see https://github.com/pgvector/pgvector", + "token_calc_max_process": "Max token computation processes (set according to expected concurrency))", + "custom_pdf_parse_url": "Custom PDF parsing URL", + "custom_pdf_parse_key": "Custom PDF parsing key", + "custom_pdf_parse_timeout": "Custom PDF parsing timeout", + "doc2x_pdf_parse_key": "Doc2x PDF parsing key (lower priority than custom PDF parsing)", + "custom_pdf_parse_price": "Custom PDF parsing price (n points/page)", + "eval_config": "Evaluation Configuration", + "eval_config_task_concurrency": "Evaluation Task Concurrency", + "eval_config_task_concurrency_desc": "Number of evaluation tasks running simultaneously", + "eval_config_case_concurrency": "Evaluation Case process Concurrency", + "eval_config_case_concurrency_desc": "Number of Evaluation Cases processed concurrently within a single task", + "eval_config_case_max_retry": "Max Retry Count of Each Evaluation Case", + "eval_config_case_max_retry_desc": "Maximum retry attempts when evaluation Case processed fail", + "eval_config_case_result_threshold": "Evaluation Judgment Default Threshold", + "eval_config_case_result_threshold_desc": "Default threshold for evaluation judgment (between 0-1, determines the positive or negative of the evaluation result)", + "eval_config_summary_concurrency": "Evaluation Report Generation Concurrency", + "eval_config_data_quality_concurrency": "Evaluation Dataset - Data Quality Concurrency", + "eval_config_dataset_synthesize_concurrency": "Evaluation Dataset - Data Synthesis Concurrency", + "eval_config_smart_generate_concurrency": "Evaluation Dataset - Data Intelligent Generation Concurrency", + "eval_config_maxStalledCount": "Evaluation - Max Stalled Job Retry Count", + "max_upload_files_per_time": "Max files per upload", + "max_upload_files_per_time_desc": "Maximum number of files per upload to a knowledge base", + "max_upload_file_size": "Max file size (MB)", + "max_upload_file_size_desc": "Maximum file size per upload to a knowledge base If you set the max file size to a large value, ensure the gateway is configured accordingly.", + "export_interval_minutes": "Export interval (minutes)", + "site_sync_interval_minutes": "Site sync interval (minutes)", + "mobile_sidebar_location": "On mobile devices, the sidebar is displayed in Account > Personal info.", + "basic_features": "Basic features", + "third_party_knowledge_base": "Third-party knowledge bases", + "third_party_publish_channels": "Third-party publishing channels", + "feature_display_config": "Display options", + "display_team_sharing": "Show content shared by team members", + "display_chat_blank_page": "Show empty chat page (disable all to hide)", + "display_invite_friends_activity": "Show invite-friends activities", + "frontend_compliance_notice": "Show compliance notice on frontend", + "feishu_knowledge_base": "Feishu", + "feishu_knowledge_base_desc": "If disabled, Feishu will no longer be displayed to users when creating knowledge bases.", + "yuque_knowledge_base": "Yuque", + "yuque_knowledge_base_desc": "If disabled, Yuque will no longer be displayed to users when creating knowledge bases.", + "feishu_publish_channel": "Feishu", + "feishu_publish_channel_desc": "If disabled, Feishu will no longer be displayed in publishing channels.", + "dingtalk_publish_channel": "DingTalk", + "dingtalk_publish_channel_desc": "If disabled, DingTalk will no longer be displayed in publishing channels.", + "wechat_publish_channel": "WeChat Official Account", + "wechat_publish_channel_desc": "If disabled, WeChat Official Account will no longer be displayed in publishing channels.", + "content_security_review": "Content security audit", + "baidu_security_id": "Baidu security ID", + "baidu_security_secret": "Baidu security secret", + "custom_security_check_url": "Custom security check URL", + "baidu_security_register_desc": "Register a baidu security verification account and create a corresponding application. Provide the application id and secret", + "custom_security_check_desc": "If you have your own security verification service, enter the address here and enable Sensitive content check on FastGPT.", + "plan_free": "Free edition", + "plan_trial": "Trial edition", + "plan_team": "Team edition", + "plan_enterprise": "Enterprise edition", + "subscription_plan": "Subscription plan", + "standard_subscription_plan": "Standard subscription plan", + "custom_plan_description": "Custom plan description", + "dataset_storage_cost_desc": "Knowledge base storage fee (xx CNY/1,000 entries/month)", + "extra_ai_points_cost_desc": "Extra AI point price (xx CNY/1000 points/month)", + "payment_method": "Payment method", + "wechat_payment_config": "WeChat payment settings", + "alipay_payment_config": "Alipay payment settings", + "corporate_payment_message": "Corporate payment notification", + "enable_subscription_plan": "Enable subscription plan", + "custom_plan_page_description": "The specified address will overwrite the address of the subscription plan page. Users will be redirected to this address when accessing the subscription plan page.", + "wechat_payment_materials": "WeChat payment documents", + "wechat_payment_registration_guide": "Register WeChat payment by yourself, currently you need to scan the QR code to pay", + "unused_field_placeholder": "Fill in anything", + "certificate_management_guide": "Obtain from WeChat Pay Merchants Platform", + "wechat_key_extraction_guide": "Follow WeChat instructions to obtain the files, and open the key file with a text editor to get the private key.", + "alipay_payment_materials": "Alipay payment documents", + "alipay_application_guide": "Register the Alipay app yourself. Currently, you need to enable payment via the computer website.", + "alipay_certificate_encryption_guide": "Use a certificate for API signing. For details, see", + "application_public_key_certificate": "App public key certificate", + "private_key_document_reference": "Refer to the above documentation for retrieving the private key", + "alipay_root_certificate": "Alipay root certificate", + "alipay_public_key_certificate": "Alipay public key certificate", + "alipay_dateway": "Alipay gateway", + "alipay_gateway_sandbox_note": "Enter the Alipay gateway URL. Use the sandbox environment for testing\nhttps://openapi-sandbox.dl.alipaydev.com/gateway.do\n,Build environment\nhttps://openapi.alipay.com/gateway.do\n", + "alipay_endpoint_sandbox_note": "Enter the Alipay endpoint URL. Use the sandbox environment for testing\nhttps://openapi-sandbox.dl.alipaydev.com\n,Build environment\nhttps://openapi.alipay.com\n", + "message_notification": "Message", + "markdown_format_support": "Supported format: Markdown", + "third_party_account_config": "Third-party accounts", + "allow_user_account_config": "Allow users to configure accounts", + "view_documentation": "View documentation", + "openai_oneapi_account": "OpenAI/OneAPI account", + "laf_account": "LAF account", + "input_laf_address": "Enter the LAF address", + "multi_team_mode": "Multi-team mode", + "single_team_mode": "Single team mode", + "sync_mode": "Sync mode", + "notification_login_settings": "Notifications & login settings", + "team_mode_settings": "Team mode settings", + "custom_user_system_config": "Custom user system settings", + "email_notification_config": "Email notification settings (registration and plan notifications)\nQQ: smtp.qq.com\ngmail: smtp.gmail.com", + "aliyun_sms_config": "Alibaba Cloud SMS settings", + "aliyun_sms_template_code": "Alibaba Cloud SMS template code (SMS_xxx)", + "wechat_service_login": "WeChat service account login", + "github_login_config": "GitHub login settings", + "google_login_config": "Google login settings", + "microsoft_login_config": "Microsoft login settings", + "quick_login": "Quick login (not recommended)", + "login_notifications_config": "Login notifications & settings", + "user_service_root_address": "User service root address (Do not add a trailing slash (/).)", + "sso_usage_guide": "For details, see [SSO & External Member Sync](https://doc.fastgpt.io/docs/guide/admin/sso/)", + "sso_login_button_title": "SSO button name", + "config_sso_login_button_title": "Configure SSO button name", + "sso_login_button_icon": "SSO button icon", + "config_sso_login_button_icon": "Configure SSO button icon", + "sso_auto_redirect": "Auto redirection for SSO", + "sso_auto_redirect_desc": "If enabled, SSO will be automatically triggered when users enter the login page.", + "email_smtp_address": "SMTP server address", + "email_smtp_address_note": "Varies from vendor to vendor.", + "email_smtp_username": "SMTP username", + "email_smtp_username_example": "Example: A QQ email address corresponds to a QQ account.", + "email_password": "Email password", + "email_smtp_auth_code": "SMTP authorization code", + "enable_email_registration": "Enable email registration", + "aliyun_access_key_guide": "Alibaba Cloud SMS parameters\nhttps://dysms.console.aliyun.com/overview\nApply for the corresponding signature and SMS template, providing:\nACCESSKEYID\nACCESSSECRET\nSignature Name\nTemplate CODE, starting with SM.", + "aliyun_secret_key": "Alibaba Cloud account secret key", + "sms_signature": "SMS signature", + "registration_account": "Sign up", + "registration_account_desc": "If configured, registration with mobile number will be enabled.", + "reset_password": "Reset password", + "reset_password_desc": "If configured, password retrieval using mobile number will be enabled.", + "bind_notification_phone": "Mobile number for receiving notifications", + "bind_notification_phone_desc": "If configured, notifications will be sent to the specified mobile number.", + "subscription_expiring_soon": "The subscription plan is about to expire.", + "subscription_expiring_soon_desc": "If configured, an SMS message will be sent when the plan is about to expire.", + "free_user_cleanup_warning": "Knowledge base cleanup alert for free edition users", + "wechat_service_appid": "App ID of WeChat service account. Enter the business edition domain for the verification address:", + "wechat_service_secret": "Secret of WeChat service account.", + "register_one": "register one", + "provide": "provide", + "domain": "domain", + "microsoft_app_client_id": "App (client) ID of Microsoft app", + "microsoft_tenant_id": "Tenant ID of Microsoft app (leave this field blank to use the default value common)", + "custom_button_name": "Custom button name", + "custom_button_name_desc": "If left blank, the default Microsoft button name will be used.", + "simple_app": "Simple app", + "workflow": "Workflow", + "plugin": "Plugin", + "folder": "Folder", + "http_plugin": "HTTP plugin", + "toolset": "toolkit", + "tool": "Tool", + "hidden": "Hideen", + "select_json_file": "Please select a JSON file.", + "confirm_delete_template": "Are you sure you want to delete the template?", + "upload_config_first": "Please upload a configuration file first.", + "app_type_not_recognized": "Unrecognized app type", + "config_json_format_error": "Invalid file format. The configuration file must be in JSON format.", + "template_update_success": "Template updated successfully.", + "template_create_success": "Template created successfully.", + "template_config": "Template settings", + "json_serialize_failed": "JSON serialization failed.", + "get_app_type_failed": "Failed to obtain application type", + "file_overwrite_content": "The file will overwrite the existing content.", + "config_file_label": "Configuration file", + "official_config": "Official configuration", + "upload_file": "Upload file", + "paste_config_or_drag_json": "Paste configuration or drag & drop a JSON file here", + "paste_config": "Paste configuration", + "app_type": "Auto-identified", + "auto_recognize": "Auto recognition", + "app_attribute_not_recognized": "Unrecognized app attribute", + "text": "Text", + "link": "Link", + "input_link": "Enter a link", + "settings_successful": "Settings saved successfully.", + "configure_quick_templates": "Configure quick template", + "search_apps": "App name", + "selected_count": "Selected: {{count}} / 3", + "category_management": "Categories", + "total_categories": "Total categories: {{length}}", + "add_category": "Add category", + "category_name": "Category name", + "category_name_empty": "Category name is required.", + "template_list": "Templates", + "quick_template": "Quick template", + "add_template": "Add template", + "recommended": "Recommended", + "no_templates": "No templates available.", + "add_attribute_first": "Please add attributes first.", + "add_plugin": "Add plugin", + "token_points": "Token points", + "token_fee_description": "If enabled, users need to pay token points and API call points to use the plugin.", + "call_points": "API call points", + "system_key": "System secret key", + "system_key_description": "For tools that require keys, you can configure system keys for them, and users can use the system keys by paying points.", + "no_plugins": "No plugins available.", + "invoice_application": "Invoice requests", + "search_user_placeholder": "Username", + "submit_status": "Request status", + "submit_complete_time": "Time requested/issued", + "invoice_title": "Invoice title", + "waiting_for_invoice": "Waiting for invoicing", + "completed": "Completed", + "confirm_invoice": "Confirm", + "no_invoice_records": "No data available.", + "invoice_details": "Invoice details", + "invoice_amount": "Invoice amount", + "organization": "Organization", + "unified_credit_code": "Unified social credit code", + "company_address": "Company address", + "company_phone": "Company phone", + "bank_name": "Bank name", + "bank_account": "Account number", + "need_special_invoice": "Require VAT invoice", + "yes": "Yes", + "no": "No", + "email_address": "Email address", + "invoice_file": "Invoice file", + "click_download": "Download", + "operation_success": "Success", + "operation_failed": "Error", + "upload_invoice_pdf": "Please upload the PDF file of your invoice", + "select_invoice_file": "Select invoice file", + "confirm_submission": "Confirm submission", + "balance_recharge": "Balance top-up", + "plan_subscription": "Plan", + "knowledge_base_expansion": "Knowledge base expansion", + "ai_points_package": "AI point package", + "monthly": "Monthly", + "yearly": "Yearly", + "free": "Free", + "trial": "Trial", + "team": "Team", + "enterprise": "Enterprise", + "custom": "Custom", + "wechat": "WeChat", + "balance": "Balance", + "alipay": "Alipay", + "corporate": "Corporate payment", + "redeem_code": "Redeem code", + "search_user": "Username", + "team_id": "Team ID", + "recharge_member_name": "Member", + "unpaid": "Unpaid", + "no_bill_records": "No data available.", + "order_details": "Order details", + "order_number": "Order ID", + "generation_time": "Time generated", + "order_type": "Order type", + "subscription_period": "Periodic", + "subscription_package": "Subscription plan", + "months": "Months", + "extra_knowledge_base_capacity": "Extra knowledge base capacity", + "extra_ai_points": "Extra AI points", + "time_error": "Start time cannot be later than end time.", + "points_error": "The remaining points cannot be greater than the total points.", + "add_success": "Added successfully.", + "add_package": "Add plan", + "package_type": "Plan type", + "basic_package": "Basic plan", + "start_time": "Start time", + "required": "*Required", + "end_time": "End time", + "package_level": "Plan level", + "total_points": "Total points", + "remaining_points": "Remaining points", + "price_yuan_for_record_only": "Price (CNY) (for record only)", + "price": "Unit price", + "update_success": "Update successful.", + "edit": "Edit", + "edit_package": "Edit plan", + "value_override_description": "The following values will overwrite default values in plan configuration. If unspecified, plan defaults will be used.", + "team_member_limit": "Max team members", + "app_limit": "Max apps", + "knowledge_base_limit": "Max knowledge bases", + "team_name": "Team name", + "points": "Points", + "start_end_time": "Period", + "version": " edition", + "extra_knowledge_base": "Extra knowledge base", + "no_package_records": "No data available.", + "role": "Permission", + "team_details": "Team details", + "chage_success": "Change saved successfully.", + "team_edit": "Edit team", + "team_list": "Teams", + "create_time": "Time created", + "no_team_data": "No data available.", + "add_user": "Add user", + "password": "Password", + "password_requirements": "Must be at least 8 characters long and contain at least 2 of the following: digits, letters, and special characters.", + "account_deactivated": "Account logged out.", + "edit_user": "Edit user", + "user_status": "Status", + "confirm_deactivate_account": "Are you sure you want to deactivate the account? Key resources of the account will be deleted, and the username will be changed to xx-deleted.", + "deactivate": "Log out", + "no_user_records": "No data available.", + "user_details": "User details", + "system_incompatibility": "The page crashed due to system incompatibility, which often occurs on Apple devices. If possible, contact the author with the operation and page details. Most cases involve Safari. Please use Chrome instead.", + "content_not_compliant": "Your content is not compliant with requirements.", + "baidu_content_security_check_exception": "Error occurred during Baidu content security check.", + "license_not_read": "Failed to read licensing information", + "license_invalid": "The license is invalid.", + "license_expired": "The license has expired.", + "license_content_error": "The license content is incorrect", + "system_not_activated": "System not activated.", + "exceed_max_users": "The number of users exceeded the maximum.", + "server_error": "Server error occurred.", + "request_error": "Request failed.", + "unknown_error": "Unknown error occurred.", + "token_expired_relogin": "Token expired. Please log in again.", + "google_verification_result": "Google verification result", + "abnormal_operation_environment": "Your operating environment is abnormal. Please refresh the page and try again, or contact Custom Service.", + "notify_expiring_packages": "Notification for expiring plans", + "notify_free_users_cleanup": "Notification for knowledge base cleanup to free edition users", + "bing_oauth_config_incomplete": "Incomplete Bing OAuth configuration.", + "request_limit_per_minute": "Max requests per minute", + "share_link_expired": "The sharing link has expired.", + "link_usage_limit_exceeded": "The link has exceeded its usage limit.", + "authentication_failed": "Identity verification failed.", + "user_registration": "Register", + "initial_password": "Your initial password is", + "notification_too_frequent": "Too many attempts.", + "emergency_notification_requires_teamid": "Team ID is required for critical notifications.", + "send_sms_failed": "Failed to send SMS message.", + "fastgpt_user": "FastGPT user", + "refund_failed": "Refund failed.", + "refund_request_failed": "Refund request failed.", + "get_certificate_list_failed": "Failed to obtain the certificate list.", + "platform_certificate_serial_mismatch": "Certificate serial numbers do not match.", + "extra_knowledge_base_storage": "Extra knowledge base storage", + "image_compression_error": "Image compression error occurred.", + "image_too_large": "The image size is too large." +} diff --git a/packages/web/i18n/en/app.json b/packages/web/i18n/en/app.json index 89c2d149ac1d..12132ab23dc3 100644 --- a/packages/web/i18n/en/app.json +++ b/packages/web/i18n/en/app.json @@ -1,6 +1,6 @@ { - "AutoOptimize": "Automatic optimization", - "Click_to_delete_this_field": "Click to delete field", + "AutoOptimize": "Auto optimize", + "Click_to_delete_this_field": "Click to delete", "Filed_is_deprecated": "The field has been deprecated.", "Index": "Index", "MCP_tools_debug": "Debug", @@ -11,27 +11,27 @@ "MCP_tools_parse_failed": "Failed to parse the MCP address.", "MCP_tools_url": "MCP address", "MCP_tools_url_is_empty": "MCP address is required.", - "MCP_tools_url_placeholder": "Enter the MCP address and then click Parse.", + "MCP_tools_url_placeholder": "Enter the MCP address and then click Parse", "No_selected_dataset": "No knowledge bases selected.", - "Optimizer_CloseConfirm": "Confirm to close", - "Optimizer_CloseConfirmText": "Optimization results have been generated, confirming that closing will lose the current result. Will it continue?", + "Optimizer_CloseConfirm": "Confirm", + "Optimizer_CloseConfirmText": "This operation will discard the optimization result. Would you like to proceed?", "Optimizer_EmptyPrompt": "Please enter optimization requirements", "Optimizer_Generating": "Generating...", - "Optimizer_Placeholder": "How do you want to write or optimize prompt words?", - "Optimizer_Placeholder_loading": "Generating...please wait", - "Optimizer_Reoptimize": "Re-optimize", - "Optimizer_Replace": "replace", - "Optimizer_Tooltip": "AI optimization prompt words", + "Optimizer_Placeholder": "Enter your suggestions for how to optimize prompts", + "Optimizer_Placeholder_loading": "Generating, please wait.", + "Optimizer_Reoptimize": "Optimize again", + "Optimizer_Replace": "Use", + "Optimizer_Tooltip": "Optimize it with AI", "Role_setting": "Permissions", "Run": "Run", "Search_dataset": "Knowledge base", "Selected": "Selected", "Team_Tags": "Team tag", - "ai_point_price": "Billing based on AI points", - "ai_settings": "AI configuration", + "ai_point_price": "Billing based on credits", + "ai_settings": "Settings", "all_apps": "All apps", "app.Version name": "Version name", - "app.error.publish_unExist_app": "Failed to publish the app. Please check whether the tool is called properly.", + "app.error.publish_unExist_app": "Failed to publish the app. Please check whether tools are called properly.", "app.error.unExist_app": "Some components are missing. Please delete them.", "app.modules.click to update": "Update", "app.modules.has new version": "New Version Available", @@ -40,57 +40,57 @@ "app.version_initial": "Initial version", "app.version_name_tips": "Version name is required.", "app.version_past": "Published", - "app.version_publish_tips": "The version will be saved to the cloud and be available to all team members. The app version for all publishing channels will be updated to this version.", + "app.version_publish_tips": "This version will be saved to the cloud, and be updated to all team members and all publishing channels.", "app_detail": "App details", "auto_execute": "Auto execution", - "auto_execute_default_prompt_placeholder": "Default questions sent during auto execution", - "auto_execute_tip": "If enabled, the workflow will be automatically executed after users enter the chat interface. Execution order: 1. Chat greetings. 2. Global variables. 3. App auto execution.", + "auto_execute_default_prompt_placeholder": "Default questions will be sent during auto execution.", + "auto_execute_tip": "If enabled, the workflow will be automatically executed when users enter the chat interface. Execution order: 1. Opening remarks. 2. Global variables. 3. App auto execution.", "auto_save": "Auto save", - "chat_debug": "Debugging preview", - "chat_logs": "Chat logs", - "chat_logs_tips": "The online chats, shared chats, and API-based chats (chat ID is required) on the app will be recorded in the logs.", - "config_ai_model_params": "Configure AI model attributes", - "config_file_upload": "Configure file upload rules", - "config_question_guide": "Configure the guess what you want feature", + "chat_debug": "Debug & preview", + "chat_logs": "Logs", + "chat_logs_tips": "The logs record online chats, shared chats, and API-based chats (chat ID is required) in the app.", + "config_ai_model_params": "Click to configure", + "config_file_upload": "Click to configure", + "config_question_guide": "Click to configure", "confirm_copy_app_tip": "An app with the same configuration except for permissions will be created. Would you like to proceed?", "confirm_del_app_tip": "Are you sure you want to delete {{name}} and its chat records?", "confirm_delete_folder_tip": "Are you sure you want to delete the folder? All apps and chat records in the folder will also be deleted. Would you like to proceed?", - "copy_one_app": "Create replica", - "core.app.QG.Switch": "Enable guess what you want", + "copy_one_app": "Duplicate", + "core.app.QG.Switch": "Status", "core.dataset.import.Custom prompt": "Custom prompt", "create_by_curl": "CURL", "create_by_template": "Template", - "create_copy_success": "Replica created successfully.", + "create_copy_success": "Duplicated successfully.", "create_empty_app": "Create blank app", "create_empty_plugin": "Create blank plugin", "create_empty_workflow": "Create blank workflow", - "cron.every_day": "Every day", - "cron.every_month": "Every month", - "cron.every_week": "Every week", + "cron.every_day": "Daily", + "cron.every_month": "Monthly", + "cron.every_week": "Weekly", "cron.interval": "Other", "dataset": "Knowledge base", - "dataset_search_tool_description": "调用语义检索、全文检索、数据库检索能力,从“知识库”中查找可能与问题相关的参考内容", + "dataset_search_tool_description": "Use semantic, full-text, and database search capabilities to search for reference materials related to the questions from the selected knowledge bases.", "day": "Day", "deleted": "The app has been deleted.", "document_quote": "Document reference", - "document_quote_tip": "It is commonly used to process content uploaded by users (document parsing is required). It can also be used to reference other string entries.", + "document_quote_tip": "Use documents uploaded by users for references (document parsing is required). It can also be used to reference other string entries.", "document_upload": "Document upload", "edit_app": "App details", - "edit_info": "Edit info", - "execute_time": "Time executed", + "edit_info": "Edit", + "execute_time": "Schedule", "export_config_successful": "The configuration has been copied, and some sensitive information has been automatically filtered out. Please check if any sensitive data still exists.", - "export_configs": "Export configuration", - "feedback_count": "User feedback", + "export_configs": "Export", + "feedback_count": "Feedback", "file_quote_link": "File link", "file_recover": "The file will overwrite the existing content.", "file_upload": "File upload", - "file_upload_tip": "If enabled, you can upload documents and images. Documents are retained for 7 days, and images for 15 days. Using this feature may generate significant additional costs. To ensure optimal user experience, when this feature is enabled, please select an AI model with a larger context length.", + "file_upload_tip": "If enabled, you can upload documents and images. Documents are retained for 7 days, while images for 15 days. Using this feature may generate significant additional costs. To ensure optimal user experience, please select a model that supports a longer context length.", "go_to_chat": "Chat now", "go_to_run": "Run now", "image_upload": "Image upload", - "image_upload_tip": "How to enable image recognition for a model", - "import_configs": "Import configuration", - "import_configs_failed": "Failed to import the configuration. Please make sure that the configuration is valid.", + "image_upload_tip": "How to enable image recognition", + "import_configs": "Import", + "import_configs_failed": "Failed to import the configuration. Please make sure that the configuration file is valid.", "import_configs_success": "Import successful.", "initial_form": "Initial status", "interval.12_hours": "Every 12 hours", @@ -100,21 +100,21 @@ "interval.6_hours": "Every 6 hours", "interval.per_hour": "Every hour", "invalid_json_format": "Please upload a valid JSON file.", - "keep_the_latest": "Auto update to the latest version", - "llm_not_support_vision": "This model does not support image recognition.", + "keep_the_latest": "Latest version", + "llm_not_support_vision": "Not supported by the current model", "llm_use_vision": "Image recognition", - "llm_use_vision_tip": "You can select a model and then check whether it supports image recognition and decide whether to enable this feature. If the image recognition feature is enabled, the model will read image contents from file links. If a question is less than 500 characters, the model will automatically parse images in the question.", - "log_chat_logs": "Chat logs", - "log_detail": "Log details", - "logs_app_data": "Data dashboard", + "llm_use_vision_tip": "Select a model and then check whether it supports image recognition. If it is enabled, the model will read image contents from file links, and automatically parse images in the question if a question is less than 500 characters.", + "log_chat_logs": "Dashboard & Logs", + "log_detail": "Logs", + "logs_app_data": "Dashboard", "logs_app_result": "App performance", "logs_average_response_time": "Avg uptime (s)", "logs_average_response_time_description": "Avg total uptime of workflow", - "logs_chat_count": "Sessions", - "logs_chat_count_description": "The total number of new sessions created in the app. A new session is created if the interval between the new message and the last message exceeds 15 minutes. (This definition only applies here.)", - "logs_chat_data": "Chat data", + "logs_chat_count": "Conversations", + "logs_chat_count_description": "The total number of new conversations created in the app. A new conversation is created if the interval between the new message and the last message exceeds 15 minutes. (This definition only applies here.)", + "logs_chat_data": "Chat analysis", "logs_chat_item_count": "Chats", - "logs_chat_item_count_description": "The total number of chats created in the app. A chat is a round of workflow execution.", + "logs_chat_item_count_description": "The total number of chats created in the app. A chat is a round of workflow execution.", "logs_chat_user": "User", "logs_date": "Date", "logs_empty": "No logs available.", @@ -122,150 +122,150 @@ "logs_error_rate": "Chat error rate", "logs_error_rate_description": "The proportion of chats that encountered error to total chats", "logs_export_confirm_tip": "The total number of chat records is {{total}}. Up to 100 latest messages can be exported from a chat. Would you like to proceed?", - "logs_export_title": "Time, Source, User, Contact info, Title, Total messages, Positive feedback, Negative feedback, Custom feedback, Marked answers, Chat details", + "logs_export_title": "Time, Source, User, Contact info, Topic, Total messages, Positive feedback, Negative feedback, Custom feedback, Marked answers, Chat details", "logs_good_feedback": "Like", - "logs_key_config": "Field configuration", + "logs_key_config": "Fields", "logs_keys_annotatedCount": "Marked answers", - "logs_keys_chatDetails": "Conversation details", + "logs_keys_chatDetails": "Chat details", "logs_keys_createdTime": "Time created", "logs_keys_customFeedback": "Custom feedback", "logs_keys_errorCount": "Errors", - "logs_keys_feedback": "User feedback", + "logs_keys_feedback": "Feedback", "logs_keys_lastConversationTime": "Last chat time", "logs_keys_messageCount": "Total messages", "logs_keys_points": "Points consumed", "logs_keys_responseTime": "Avg response time", - "logs_keys_sessionId": "Session ID", + "logs_keys_sessionId": "Conversation ID", "logs_keys_source": "Source", - "logs_keys_title": "Title", + "logs_keys_title": "Topic", "logs_keys_user": "User", "logs_message_total": "Total messages", "logs_new_user_count": "New users", "logs_points": "Points consumed", "logs_points_description": "Points consumed by the app", - "logs_points_per_chat": "Avg points consumed per session", - "logs_points_per_chat_description": "Avg points consumed per workflow execution", + "logs_points_per_chat": "Avg credits consumed per conversation", + "logs_points_per_chat_description": "Avg credits consumed per workflow execution", "logs_response_time": "Avg response time", - "logs_search_chat": "Session title or ID", + "logs_search_chat": "Conversation topic or ID", "logs_source": "Source", - "logs_source_count_description": "Users by channel", - "logs_title": "Title", + "logs_source_count_description": "Users by publishing channel", + "logs_title": "Topic", "logs_total": "Total", "logs_total_avg_points": "Avg points consumed", - "logs_total_chat": "Total chats", + "logs_total_chat": "Chats", "logs_total_error": "Total errors: {{count}}, Error rate: {{rate}}%", - "logs_total_points": "Total points consumed", + "logs_total_points": "Total credits consumed", "logs_total_tips": "Cumulative metrics are not affected by the time filter.", - "logs_total_users": "Total users", + "logs_total_users": "Users", "logs_user_count": "Users", - "logs_user_count_description": "The number of users who created chats in the app in the period", - "logs_user_data": "User data", - "logs_user_feedback": "User feedback", + "logs_user_count_description": "The number of daily users who created chats in the app", + "logs_user_data": "User analysis", + "logs_user_feedback": "Feedback", "logs_user_feedback_description": "Likes: Number of user likes\nDislikes: Number of user dislikes", "logs_user_retention": "Retained users", "logs_user_retention_description": "The number of new users in period T who were active in period T+1", - "look_ai_point_price": "View billing standard by model", + "look_ai_point_price": "View billing standard for models", "manual_secret": "Temporary secret key", "mark_count": "Marked answers", "max_histories_number": "Chats remembered", - "max_histories_number_tip": "The maximum number of chats that a model can remember. If a chat exceeds the max context length, the system will automatically delete the excess part. Therefore, the model may not actually remember 30 chats even if you set this field to 30.", + "max_histories_number_tip": "The maximum number of chats that a model can remember. If a chat exceeds the max context length, the system will automatically delete the excess part. Therefore, the model may not actually remember 30 chats even if this field is set to 30.", "max_tokens": "Max tokens", - "module.Custom Title Tip": "The title will be displayed in the chat.", + "module.Custom Title Tip": "The topic will be displayed in the chat.", "module.No Modules": "Plugin not found.", "module.type": "{{type}} type\n{{description}}", "modules.Title is required": "Module name is required.", "month.unit": "Day", - "move.hint": "If moved, the app or folder will inherit the permissions of the destination folder, and its current permissions will become invalid.", - "move_app": "Move app", + "move.hint": "If moved, the app or folder will inherit the permissions of the destination folder, invalidating its current permissions.", + "move_app": "Move", "no_mcp_tools_list": "No data available. The MCP address must be parsed first.", - "node_not_intro": "No description available for this node.", + "node_not_intro": "No description available.", "not_json_file": "Please select a JSON file.", "not_the_newest": "Earlier version", - "oaste_curl_string": "Enter CURL code", - "open_auto_execute": "Enable auto execute", - "open_vision_function_tip": "A model with a switch displayed supports image recognition. If enabled, the model will parse images from file links and will automatically parse images in the question (it takes effect when the question is less than 500 characters).", + "oaste_curl_string": "Enter CURL command", + "open_auto_execute": "Status", + "open_vision_function_tip": "A model with a switch toggled on, as shown below, supports image recognition. If enabled, the model will automatically parse images from file links and images in the questions with less than 500 characters.", "or_drag_JSON": "Drag & drop to upload JSON file", "paste_config_or_drag": "Paste configuration or drag & drop a JSON file here", "pdf_enhance_parse": "Enhanced PDF parsing", - "pdf_enhance_parse_price": "{{price}} points/page", - "pdf_enhance_parse_tips": "PDF recognition model supports parsing PDF files, converting PDF files into Markdown format with images preserved, and processing scanned copies of PDF files, which takes a longer time.", - "permission.des.manage": "Has write permissions and can configure publishing channels, view chat logs, and assign app permissions.", + "pdf_enhance_parse_price": "{{price}} credits/page", + "pdf_enhance_parse_tips": "Call a PDF recognition model to parse PDF files, converting them into Markdown format with images preserved, and to process scanned copies of PDF files, which takes longer time.", + "permission.des.manage": "Has write permission and can configure publishing channels, view chat logs, and assign app permissions.", "permission.des.read": "Chat using the app.", - "permission.des.readChatLog": "View chat logs.", - "permission.des.write": "View and edit the app.", + "permission.des.readChatLog": "View chat logs", + "permission.des.write": "View and edit the app", "permission.name.read": "Chat only", "permission.name.readChatLog": "View chat logs", "plugin.Instructions": "Guide", "plugin_cost_by_token": "Billing based on token consumption", - "plugin_cost_folder_tip": "The toolkit contains multiple tools. The points are charged based on the tools used.", - "plugin_cost_per_times": "{{cost}} points/call", + "plugin_cost_folder_tip": "The toolkit contains multiple tools. The credits are consumed based on the tools used.", + "plugin_cost_per_times": "{{cost}} credits/call", "plugin_dispatch": "Plugin call", - "plugin_dispatch_tip": "Enable the model to obtain external data. The model will automatically call plugins as needed, and all plugins will run in non-streaming mode.\nSelected plugins will be automatically called when the knowledge base is called.", + "plugin_dispatch_tip": "Enable the model to obtain external data. The model will automatically call tools as needed, and run them in non-streaming mode.\nSelected plugins will be automatically called when the knowledge base is called.", "pro_modal_feature_1": "Connection to external organizations and multi-tenancy", "pro_modal_feature_2": "Dedicated app page for team", - "pro_modal_feature_3": "Enhanced knowledge base index", + "pro_modal_feature_3": "Enhanced knowledge base indexing", "pro_modal_later_button": "Not now", - "pro_modal_subtitle": "Subscribe to the enterprise edition for advanced features", - "pro_modal_title": "Exclusive to enterprise edition", + "pro_modal_subtitle": "Subscribe to the commercial edition for advanced features", + "pro_modal_title": "Exclusive to commercial edition", "pro_modal_unlock_button": "Subscribe", - "publish_channel": "Publishing channel", + "publish_channel": "Publishing channels", "publish_success": "Published successfully.", - "question_guide_tip": "Three suggested questions will be generated for you at the end of the chat.", - "reasoning_response": "Output reasoning process", + "question_guide_tip": "Three relevant question suggestions will be automatically generated for you after the conversation.", + "reasoning_response": "Output reasoning", "response_format": "Response format", - "save_team_app_log_keys": "Save as team configuration", - "saved_success": "Changes saved successfully. Changes saved successfully. To make the version available externally, please click Save and publish.", + "save_team_app_log_keys": "Save as team config", + "saved_success": "Changes saved successfully. To make the version available to others, please click Save and publish.", "search_app": "App name", "search_tool": "Tool name", "secret_get_course": "Guide", - "setting_app": "Application configuration", - "setting_plugin": "Plugin configuration", + "setting_app": "App settings", + "setting_plugin": "Plugin settings", "show_top_p_tip": "Nucleus sampling is an alternative to temperature sampling. By using this method, the model considers only tokens whose cumulative probability exceeds p. For example, if p is set to 0.1, the model will consider only tokens with the highest cumulative probability. Default value: 1.", "simple_tool_tips": "The plugin cannot be called by simple apps because it contains special inputs.", "source_updateTime": "Time updated", "stop_sign": "Stop sequence", "stop_sign_placeholder": "Separate multiple sequences with the pipe character (|). Example: aaa|stop", - "stream_response": "Output in streaming mode", - "stream_response_tip": "If disabled, the model must output contents in non-streaming mode, and the contents will not be output directly. You can obtain the output contents for further processing.", - "sync_log_keys_popover_text": "The field configuration will take effect only on your account. Would you like to save it as team configuration?", - "sync_team_app_log_keys": "Restore team configuration", + "stream_response": "Stream output", + "stream_response_tip": "If disabled, the model will not output contents immediately, but wait until the entire response is generated. Therefore, it can obtain the output contents for further processing.", + "sync_log_keys_popover_text": "The field configuration will apply only to your account. Would you like to save it as the team configuration?", + "sync_team_app_log_keys": "Restore to team config", "system_secret": "System secret key", "systemval_conflict_globalval": "The variable name conflicts with a system variable name. Please use another one.", "team_tags_set": "Team tag", "temperature": "Temperature", - "temperature_tip": "Range: 0-10. A higher value leads to more creative outputs, while a lower value leads to more deterministic outputs.", + "temperature_tip": "Range: 0-10 A higher value leads to more creative outputs, while a lower value leads to more deterministic outputs.", "template.hard_strict": "Strict Q&A template", - "template.hard_strict_des": "A template based on Q&A template. It enforces stricter requirements on the model's answers.", + "template.hard_strict_des": "Enforces stricter requirements on the model's answers based on Q&A templates.", "template.qa_template": "Q&A template", - "template.qa_template_des": "A template used for knowledge bases with a Q&A structure. With this template enabled, the model can be configured to output based on the predefined content.", + "template.qa_template_des": "Requires the model to output based on the predefined content, used for knowledge bases with a Q&A structure.", "template.simple_robot": "Simple bot", "template.standard_strict": "Strict standard template", - "template.standard_strict_des": "A template based on standard template. It enforces stricter requirements on the model's answers.", + "template.standard_strict_des": "Enforces stricter requirements on the model's answers based on standard templates.", "template.standard_template": "Standard template", - "template.standard_template_des": "A template that contains standard prompts. It is used for knowledge bases with no fixed structure.", + "template.standard_template_des": "Contains standard prompts, used for knowledge bases with no fixed structure.", "templateMarket.Search_template": "Template", "templateMarket.Use": "Use", - "templateMarket.no_intro": "No data available.", + "templateMarket.no_intro": "No description available.", "templateMarket.templateTags.Image_generation": "Image generation", "templateMarket.templateTags.Office_services": "Office service", - "templateMarket.templateTags.Recommendation": "Recommendations", + "templateMarket.templateTags.Recommendation": "Recommended", "templateMarket.templateTags.Roleplay": "Role playing", - "templateMarket.templateTags.Web_search": "Online search", + "templateMarket.templateTags.Web_search": "Search", "templateMarket.templateTags.Writing": "Content creation", "templateMarket.template_guide": "Template description", "template_market": "Templates", - "template_market_description": "Come to explore more possibilities with templates. Follow the configuration and usage guides to develop apps using templates.", - "template_market_empty_data": "No suitable template available.", + "template_market_description": "Explore more possibilities with templates. Follow the configuration and usage guides to develop apps using templates.", + "template_market_empty_data": "No template found.", "time_zone": "Time zone", "too_to_active": "Activate", "tool_active_manual_config_desc": "The temporary secret key is stored in the app and can only be used by the app.", "tool_active_system_config_desc": "Use the system-configured key", - "tool_active_system_config_price_desc": "Additional price for using the secret key ({{price}} points/call).", - "tool_active_system_config_price_desc_folder": "Additional costs for using the secret key is required. The fee is charged based on tool usage.", + "tool_active_system_config_price_desc": "Additional price for using the secret key ({{price}} credits/call).", + "tool_active_system_config_price_desc_folder": "Charged based on the tool usage", "tool_detail": "Tool details", "tool_input_param_tip": "To run the plugin properly, please configure the required information.", - "tool_not_active": "The tool has not been activated.", - "tool_run_free": "Running the tool does not consume points.", + "tool_not_active": "Not activated.", + "tool_run_free": "Running the tool does not consume credits.", "tool_type_communication": "Communications", "tool_type_design": "Design", "tool_type_entertainment": "Business", @@ -275,30 +275,30 @@ "tool_type_productivity": "Productivity", "tool_type_scientific": "Scientific research", "tool_type_search": "Search", - "tool_type_social": "Social networking", - "tool_type_tools": "Tools", - "tools_no_description": "No description available for this tool.", + "tool_type_social": "Social", + "tool_type_tools": "Tool", + "tools_no_description": "No description available.", "transition_to_workflow": "Convert to workflow", - "transition_to_workflow_create_new_placeholder": "Create a new app instead of modifying the current one.", - "transition_to_workflow_create_new_tip": "If converted to a workflow app, it cannot be reverted to simple mode. Would you like to proceed?", + "transition_to_workflow_create_new_placeholder": "Create a new app instead of modifying the current one", + "transition_to_workflow_create_new_tip": "If converted, it cannot be reverted to a simple app. Would you like to proceed?", "tts_ai_model": "Use text-to-speech model", "tts_browser": "Built-in in browser (free)", "tts_close": "Close", "type.All": "All", - "type.Create http plugin tip": "Bulk create plugins by using OpenAPI schema, The plugins to be created must be compatible with the GPTs format.", + "type.Create http plugin tip": "Bulk create plugins using OpenAPI schema. They must be compatible with the GPTs format.", "type.Create mcp tools tip": "Automatically parse the specified MCP address to bulk create callable MCP tools.", - "type.Create one plugin tip": "Encapsulate reusable processes of a workflow, with configurable inputs and outputs.", + "type.Create one plugin tip": "Encapsulate reusable processes of a workflow, featuring custom inputs and outputs.", "type.Create plugin bot": "Create plugin", "type.Create simple bot": "Create simple app", - "type.Create simple bot tip": "Create a simple AI app by using a guided form, which is ideal for beginners.", + "type.Create simple bot tip": "Create a simple AI app using a template, designed for beginners.", "type.Create workflow bot": "Create workflow", - "type.Create workflow tip": "Create a complex AI app that can handle multiple rounds of chats by using low-code methods, which is recommended for advanced users.", + "type.Create workflow tip": "Create a complex AI app capable of multi-round dialogue tasks using low-code methods, designed for advanced users.", "type.Folder": "Folder", "type.Http plugin": "HTTP plugin", - "type.Import from json": "Import JSON configuration file", - "type.Import from json tip": "Create an app using a JSON configuration file.", + "type.Import from json": "Import", + "type.Import from json tip": "Create an app by importing a JSON configuration file.", "type.Import from json_error": "Failed to obtain workflow data. Please check the URL or paste the JSON data.", - "type.Import from json_loading": "Obtaining workflow data, please wait.", + "type.Import from json_loading": "Loading, please wait.", "type.MCP tools": "MCP toolkit", "type.MCP_tools_url": "MCP address", "type.Plugin": "Plugin", @@ -313,15 +313,15 @@ "un_auth": "No permission", "upload_file_max_amount": "Max files", "upload_file_max_amount_tip": "The maximum number of files that can be uploaded in a chat", - "variable.select type_desc": "You can configure global variables for a workflow. These global variables are typically used for temporary caching. Variables can be specified in the following ways:\n1. Use query parameters on the chat page.\n2. Use variables in the API request.\n3. Use a node with the variable update tool.", - "variable.textarea_type_desc": "Up to 4,000 characters can be entered in the input box.", + "variable.select type_desc": "Configure global variables for a workflow. These global variables are typically used for temporary caching. Variables can be specified in the following ways:\n1. Use query parameters on the chat page.\n2. Use variables in the API request.\n3. Use a node with the variable update tool.", + "variable.textarea_type_desc": "Up to 4,000 characters allowed.", "variable_name_required": "Variable name is required.", "variable_repeat": "The variable name already exists.", "version.Revert success": "Rollback successful.", "version_back": "Roll back to initial status", "version_copy": "Replica", "version_initial_copy": "Replica - Initial status", - "vision_model_title": "Image recognition capability", + "vision_model_title": "Image recognition", "week.Friday": "Friday", "week.Monday": "Monday", "week.Saturday": "Saturday", @@ -334,23 +334,32 @@ "workflow.form_input": "Form input", "workflow.form_input_description_placeholder": "Example:\nComplete your information", "workflow.form_input_tip": "This module can be configured with multiple input prompts to guide users in entering specific content.", - "workflow.input_description_tip": "You can add a description to explain to users what they need to enter.", + "workflow.input_description_tip": "Add a description to explain to users what they need to enter.", "workflow.read_files": "Document parsing", - "workflow.read_files_result": "Result for document parsing", - "workflow.read_files_result_desc": "A file contains a filename and the content. Separate multiple files with hyphens.", - "workflow.read_files_tip": "Parse the file uploaded in the current chat and return the result.", + "workflow.read_files_result": "Document parsing result", + "workflow.read_files_result_desc": "A file consists of a file name and the content. Separate multiple files with hyphens.", + "workflow.read_files_tip": "Parse the file uploaded in the current conversation and return the result.", "workflow.select_description": "Description", "workflow.select_description_placeholder": "Example:\nIs there any tomato in the refrigerator?", - "workflow.select_description_tip": "You can add a description to explain to users the definition of each option.", + "workflow.select_description_tip": "Add a description to explain to users the definition of each option.", "workflow.select_result": "Select variable name", "workflow.user_file_input": "File link", - "workflow.user_file_input_desc": "File and image link uploaded", + "workflow.user_file_input_desc": "File and image link uploaded by users", "workflow.user_select": "User selection", - "workflow.user_select_tip": "The module can be configured with multiple options and can be selected during the chat. Different options direct the chat to different workflow branches.", - "files_cascader_no_knowledge_base": "不加入知识库", - "files_cascader_select_knowledge_base": "请选择知识库", - "files_cascader_select_first": "请先选择知识库", - "files_cascader_dataset_empty": "该知识库数据集为空", - "select_join_location": "选择加入位置", - "no_data_for_smart_generate": "该知识库中没有可用于智能生成的数据" -} + "workflow.user_select_tip": "You can configure multiple options for users to select during the chat. Different options direct the chat to different workflow branches.", + "files_cascader_no_knowledge_base": "Do not add to knowledge base", + "files_cascader_select_knowledge_base": "Select", + "files_cascader_select_first": "Please select a knowledge base first.", + "files_cascader_dataset_empty": "This knowledge base dataset is empty.", + "select_join_location": "Location", + "no_data_for_smart_generate": "该知识库中没有可用于智能生成的数据", + "logs_bad_feedback": "点踩", + "logs_source_count": "渠道用户", + "logs_timespan_day": "按日", + "logs_timespan_month": "按月", + "logs_timespan_quarter": "按季", + "logs_timespan_week": "按周", + "logs_total_avg_duration": "平均时长", + "logs_total_feedback": "共 {{goodFeedBack}} 赞 | 共 {{badFeedBack}} 踩", + "logs_user_callback": "用户反馈" +} \ No newline at end of file diff --git a/packages/web/i18n/en/chat.json b/packages/web/i18n/en/chat.json index c774b0397803..d8ee99b1132f 100644 --- a/packages/web/i18n/en/chat.json +++ b/packages/web/i18n/en/chat.json @@ -1,6 +1,6 @@ { - "AI_input_is_empty": "The content passed to the AI node is empty.", - "Delete_all": "Clear word library", + "AI_input_is_empty": "The content passed to the node is empty.", + "Delete_all": "Clear", "LLM_model_response_empty": "The output of the model in streaming mode is empty. Please check whether the model output works properly in streaming mode.", "ai_reasoning": "Reasoning process", "back_to_text": "Return to input", @@ -8,57 +8,57 @@ "chat.quote.deleted": "The data has been deleted.", "chat.waiting_for_response": "Please wait for the chat to complete.", "chat_history": "Chat records", - "chat_input_guide_lexicon_is_empty": "No word library is configured.", + "chat_input_guide_lexicon_is_empty": "No word library available.", "chat_test_app": "Debug - {{name}}", "citations": "References: {{num}}", "click_contextual_preview": "Click to preview context", "completion_finish_close": "Disconnected", - "completion_finish_content_filter": "Safety control was triggered.", + "completion_finish_content_filter": "Content security audit triggered", "completion_finish_function_call": "Function call", "completion_finish_length": "The output has exceeded the maximum.", "completion_finish_null": "Unknown", "completion_finish_reason": "Completion cause", "completion_finish_stop": "Completed successfully.", "completion_finish_tool_calls": "Tool call", - "config_input_guide": "Configure input guide", - "config_input_guide_lexicon": "Configure word library", - "config_input_guide_lexicon_title": "Configure word library", + "config_input_guide": "Click to configure", + "config_input_guide_lexicon": "Settings", + "config_input_guide_lexicon_title": "Settings", "content_empty": "Content is empty.", "contextual": "{{num}} contexts", "contextual_preview": "Contexts previewed: {{num}}", "core.chat.moveCancel": "Swipe up to cancel", "core.chat.shortSpeak": "The speech is too short.", - "csv_input_lexicon_tip": "Only CSV files can be bulk imported. Click to download the template", - "custom_input_guide_url": "Custom word library IP address", + "csv_input_lexicon_tip": "Only CSV files can be imported. Click to download the template.", + "custom_input_guide_url": "Custom word library address", "data_source": "Source knowledge base: {{name}}", "dataset_quote_type error": "Incorrect reference type of knowledge base. Correct type: { datasetId: string }[]", - "delete_all_input_guide_confirm": "Are you sure you want to clear the input guide library?", - "download_chunks": "Download data", + "delete_all_input_guide_confirm": "Are you sure you want to clear the word library?", + "download_chunks": "Download", "empty_directory": "No items selectable in the directory.", "error_message": "Error details", "file_amount_over": "The number of files exceeds the maximum ({{max}}).", "file_input": "System file", - "file_input_tip": "You can obtain the required file link through the file link field in the plugin start node.", + "file_input_tip": "You can obtain the required file link through the file link field in the plugin input node.", "history_slider.home.title": "Chat", "home.chat_app": "Home page chat - {{name}}", - "home.chat_id": "Session ID", + "home.chat_id": "Conversation ID", "home.no_available_tools": "No tools available.", "home.select_tools": "Select", "home.tools": "Tools: {{num}}", "in_progress": "Ongoing", "input_guide": "Input guide", "input_guide_lexicon": "Word library", - "input_guide_tip": "You can configure some preset questions. When a user enters a question, related preset questions will be displayed as prompts.", + "input_guide_tip": "Configure preset questions. When a user enters a question, related preset questions will be displayed as prompts.", "input_placeholder_phone": "Enter question", - "insert_input_guide,_some_data_already_exists": "Duplicate entries have been detected and automatically filtered. {{len}} entries have been inserted.", + "insert_input_guide,_some_data_already_exists": "Duplicate entries were detected and automatically filtered. {{len}} entries were inserted.", "invalid_share_url": "Invalid sharing link.", "is_chatting": "Chatting, please wait.", "items": "Items", "llm_tokens": "LLM tokens", "module_runtime_and": "Total workflow uptime", "multiple_AI_conversations": "Multiple AI chats", - "new_input_guide_lexicon": "New word library", - "no_invalid_app": "No apps available for your account.", + "new_input_guide_lexicon": "New word", + "no_invalid_app": "No apps available.", "no_workflow_response": "No running data available.", "not_query": "Query content is missing.", "not_select_file": "No file is selected.", @@ -66,7 +66,7 @@ "press_to_speak": "Hold to talk", "query_extension_IO_tokens": "Input/output tokens for question optimization", "query_extension_result": "Question optimization result", - "question_tip": "Modules respond in a top-down sequence.", + "question_tip": "The following shows the response sequence.", "read_raw_source": "View source text", "reasoning_text": "Reasoning process", "release_cancel": "Release to cancel", @@ -82,50 +82,54 @@ "select": "Select", "select_file": "Upload file", "select_file_img": "Upload file/image", - "select_img": "Upload Image", + "select_img": "Upload image", "setting.copyright.basic_configuration": "Basics", "setting.copyright.copyright_configuration": "Copyright", - "setting.copyright.diagram": "Illustration", + "setting.copyright.diagram": "Preview", "setting.copyright.file_size_exceeds_limit": "The file size exceeds the maximum ({{maxSize}}).", "setting.copyright.immediate_upload_required": "Upload an image to use the feature.", "setting.copyright.logo": "Logo", "setting.copyright.preview_fail": "Failed to preview the file.", - "setting.copyright.save_fail": "Failed to save the Logo.", + "setting.copyright.save_fail": "Failed to save the logo.", "setting.copyright.save_success": "Logo saved successfully.", "setting.copyright.select_logo_image": "Please select a logo image first.", - "setting.copyright.style_diagram": "Illustration", + "setting.copyright.style_diagram": "Preview", "setting.copyright.tips": "Recommended ratio: 4:1", "setting.copyright.tips.square": "Recommended ratio: 1:1", "setting.copyright.title": "Copyright info", "setting.copyright.upload_fail": "Failed to upload the file.", - "setting.data_dashboard.title": "Data dashboard", + "setting.data_dashboard.title": "Dashboard", "setting.fastgpt_chat_diagram": "/imgs/chat/fastgpt_chat_diagram.png", "setting.home.available_tools.add": "Add", - "setting.home.commercial_version": "Enterprise edition", - "setting.home.diagram": "Illustration", + "setting.home.commercial_version": "Commercial edition", + "setting.home.diagram": "Preview", "setting.home.dialogue_tips": "Chat box prompt", "setting.home.dialogue_tips.default": "You can ask me anything.", - "setting.home.dialogue_tips_placeholder": "Please enter the prompt text of the dialog box", - "setting.home.home_tab_title": "Home page title", - "setting.home.home_tab_title_placeholder": "Home page title is required.", + "setting.home.dialogue_tips_placeholder": "", + "setting.home.home_tab_title": "Homepage title", + "setting.home.home_tab_title_placeholder": "Homepage title is required.", "setting.home.slogan": "Slogan", "setting.home.slogan.default": "Hi 👋, I'm FastGPT. How can I help you today?", - "setting.home.slogan_placeholder": "Please enter Slogan", - "setting.home.title": "Home page configuration", + "setting.home.slogan_placeholder": "", + "setting.home.title": "Homepage configuration", "setting.incorrect_plan": "The current plan does not support this feature. Please upgrade your subscription plan.", "setting.incorrect_version": "The current version does not support this feature.", - "setting.log_details.title": "Home page logs", - "setting.logs.title": "Home page logs", + "setting.log_details.title": "Logs", + "setting.logs.title": "Logs", "setting.save": "Save", "setting.save_success": "Changes saved successfully.", "sidebar.home": "Home", - "sidebar.team_apps": "Team app", + "sidebar.team_apps": "Team apps", "source_cronJob": "Scheduled execution", "start_chat": "Chat now", - "stream_output": "Output in streaming mode", + "stream_output": "Stream output", "unsupported_file_type": "File type is not supported.", "upload": "Upload", - "variable_invisable_in_share": "Custom variables are not visible in login-free mode.", + "variable_invisable_in_share": "Custom variables are not visible in the login-free link.", "view_citations": "View reference", - "web_site_sync": "Website sync" -} + "web_site_sync": "Website sync", + "setting.home.available_tools": "可用工具", + "setting.share": "分享", + "embedding_model_error": "向量模型出错,请核查模型配置信息", + "language_model_error": "请求大模型出错,请检查模型配置是否正确" +} \ No newline at end of file diff --git a/packages/web/i18n/en/common.json b/packages/web/i18n/en/common.json index ba5b290e3d02..f1225ea03e11 100644 --- a/packages/web/i18n/en/common.json +++ b/packages/web/i18n/en/common.json @@ -6,12 +6,12 @@ "App": "App", "Cancel": "Cancel", "Choose": "Select", - "Click_to_expand": "View details", + "Click_to_expand": "Click to view details.", "Close": "Close", "Code": "Source code", "Config": "Configuration", "Confirm": "OK", - "Continue_Adding": "Add more", + "Continue_Adding": "Save & add more", "Copy": "Copy", "Creating": "Creating", "Delete": "Delete", @@ -23,20 +23,20 @@ "Error": "Error", "Exit": "Exit", "Export": "Export", - "FAQ.ai_point_a": "Each AI model call consumes AI points. For details, see the billing standard by using AI points above. The system prioritizes the usage data returned by the model provider. If no usage data is returned, it estimates token consumption based on the calculation method of GPT-3.5: 1 token ≈ 0.7 Chinese characters ≈ 0.9 English words. Consecutive characters may be counted as 1 token.", - "FAQ.ai_point_expire_a": "Yes. After the current plan expires, AI points will be cleared and updated according to the new plan. The points for an annual plan are valid for one year and are not reset monthly.", - "FAQ.ai_point_expire_q": "Do AI points expire?", - "FAQ.ai_point_q": "What are AI points?", - "FAQ.check_subscription_a": "On the Account > Personal info > Plan details > Usage page, you can view the valid and expiration time of your plans. The paid plan will automatically switch to the free one after expiration.", + "FAQ.ai_point_a": "Model calls consume credits. For details, refer to the billing standard above. The system prioritizes the usage data returned by the model provider. If no usage data is returned, it estimates token consumption using the calculation method of GPT-3.5: 1 token ≈ 0.7 Chinese characters ≈ 0.9 English words. Consecutive characters may be counted as 1 token.", + "FAQ.ai_point_expire_a": "Yes. After the current plan expires, credits will be cleared and updated according to the new plan. The credits for an annual plan are valid for one year.", + "FAQ.ai_point_expire_q": "Do credits expire?", + "FAQ.ai_point_q": "What are credits?", + "FAQ.check_subscription_a": "On the Account > Profile > Plan details > Usage page, you can view the valid and expiration time of your plans. The paid plan will automatically switch to the free edition after expiration.", "FAQ.check_subscription_q": "Where can I view my subscription plans?", - "FAQ.dataset_compute_a": "One entry stored in the knowledge base equals one knowledge base index. One chunked entry usually corresponds to multiple indexes. Therefore, n sets of indexes can be found in one knowledge base collection.", + "FAQ.dataset_compute_a": "One entry stored in the knowledge base equals one knowledge base index. One chunked entry usually corresponds to multiple indexes. Therefore, n groups of indexes can be found in one knowledge base collection.", "FAQ.dataset_compute_q": "How is knowledge base storage calculated?", "FAQ.dataset_index_a": "No. However, you cannot insert or update any content in the knowledge base.", - "FAQ.dataset_index_q": "Will existing entries in a knowledge base be deleted when the number of entries exceeds the maximum?", - "FAQ.free_user_clean_a": "If a team using the free edition without any extra subscription plans does not log in for 30 consecutive days, the system will automatically clear all knowledge base content belonging to the team.", - "FAQ.free_user_clean_q": "Will data be cleared in the free edition?", - "FAQ.package_overlay_a": "Yes. Purchased resource packages are independent and can be used concurrently within their validity periods. AI points from the resource package that expires first will be consumed first.", - "FAQ.package_overlay_q": "Can multiple extra resource packages be used concurrently?", + "FAQ.dataset_index_q": "Will extra entries in a knowledge base be deleted when the number of entries exceeds the maximum?", + "FAQ.free_user_clean_a": "If a team using the free edition without any other subscription plans is inactive for 30 consecutive days, its knowledge bases will be cleared automatically.", + "FAQ.free_user_clean_q": "Will data in the free edition be cleared?", + "FAQ.package_overlay_a": "Yes. Purchased resource packages are independent and can be used concurrently within their validity periods. Credits for the resource package that expires first will be consumed first.", + "FAQ.package_overlay_q": "Can multiple additional resource packages be used concurrently?", "FAQ.switch_package_a": "Subscription plan usage follows a priority rule where a plan with higher level is used first. Therefore, if the level of a new plan is higher than the current one, it takes effect immediately. Otherwise, the current plan will not be switched.", "FAQ.switch_package_q": "Can I switch subscription plans?", "File": "File", @@ -75,7 +75,7 @@ "Run": "Run", "Running": "Running", "Save": "Save", - "Save_and_exit": "Save and exit", + "Save_and_exit": "Save & exit", "Search": "Search", "Select_App": "Select", "Select_all": "Select all", @@ -90,7 +90,7 @@ "Update": "Update", "Username": "Username", "Waiting": "Waiting", - "Warning": "Message", + "Warning": "Warning", "Website": "Website", "action_confirm": "Confirm", "add_new": "Add", @@ -109,15 +109,15 @@ "bill_already_processed": "Processed", "bill_expired": "Expired", "bill_not_pay_processed": "Offline", - "button.extra_dataset_size_tip": "You are purchasing extra knowledge base capacity.", - "button.extra_points_tip": "You are purchasing extra AI points.", + "button.extra_dataset_size_tip": "You are purchasing additional knowledge base indexes.", + "button.extra_points_tip": "You are purchasing additional credits.", "can_copy_content_tip": "Auto copy is unavailable. Please manually copy the content below.", "chart_mode_cumulative": "Cumulative", "chart_mode_incremental": "Periodic", - "chat": "Session", - "chat_chatId": "Session ID: {{chatId}}", + "chat": "Conversation", + "chat_chatId": "Conversation ID: {{chatId}}", "choosable": "Available", - "chose_condition": "Filter", + "chose_condition": "Condition", "chosen": "Selected", "classification": "Classification", "click_drag_tip": "Click and drag", @@ -128,11 +128,11 @@ "code_error.account_error": "Username or password is invalid.", "code_error.account_exist": "The account already exists.", "code_error.account_not_found": "The account does not exist.", - "code_error.app_error.can_not_edit_admin_permission": "Administrator permissions cannot be edited.", + "code_error.app_error.can_not_edit_admin_permission": "Admin permissions cannot be edited.", "code_error.app_error.invalid_app_type": "Invalid app type.", "code_error.app_error.invalid_owner": "Invalid app owner.", "code_error.app_error.not_exist": "The app does not exist.", - "code_error.app_error.un_auth_app": "You do not have permission to perform operation on the app.", + "code_error.app_error.un_auth_app": "You do not have permission to perform operations on the app.", "code_error.chat_error.un_auth": "You do not have permission to perform operations on the chat record.", "code_error.error_code.400": "Request failed.", "code_error.error_code.401": "You do not have access permission.", @@ -149,33 +149,33 @@ "code_error.error_code.429": "Too many requests.", "code_error.error_message.403": "Credential error.", "code_error.error_message.510": "The account balance is insufficient.", - "code_error.error_message.511": "You do not have permission to perform operations on the model.", + "code_error.error_message.511": "You do not have permission on the model.", "code_error.error_message.513": "You do not have permission to read the file.", "code_error.error_message.514": "Invalid API key.", "code_error.openapi_error.api_key_not_exist": "The API key does not exist.", "code_error.openapi_error.exceed_limit": "Up to 10 API keys can be created.", - "code_error.openapi_error.un_auth": "You do not have permission to perform operations on the API key.", + "code_error.openapi_error.un_auth": "You do not have permission on the API key.", "code_error.outlink_error.invalid_link": "Invalid sharing link.", "code_error.outlink_error.link_not_exist": "The sharing link does not exist.", "code_error.outlink_error.un_auth_user": "Identity verification failed.", "code_error.plugin_error.not_exist": "The tool does not exist.", - "code_error.plugin_error.un_auth": "You do not have permission to perform operations on the tool.", - "code_error.system_error.community_version_num_limit": "The number of resources exceeded the maximum allowed by the community edition. Please upgrade to the enterprise edition: https://fastgpt.in", + "code_error.plugin_error.un_auth": "You do not have permission on the tool.", + "code_error.system_error.community_version_num_limit": "The number of resources exceeded the maximum allowed by the community edition. Please upgrade to the commercial edition: https://fastgpt.in", "code_error.system_error.license_app_amount_limit": "The number of apps has exceeded the maximum.", "code_error.system_error.license_dataset_amount_limit": "The number of knowledge bases has exceeded the maximum.", "code_error.system_error.license_user_amount_limit": "The number of users has exceeded the maximum.", "code_error.system_error.license_evaluation_task_amount_limit": "The number of evaluation tasks has exceeded the maximum.", "code_error.system_error.license_eval_dataset_amount_limit": "The number of evaluation datasets has exceeded the maximum.", - "code_error.system_error.license_eval_dataset_data_amount_limit": "The number of evaluation dataset data entries has exceeded the maximum.", + "code_error.system_error.license_eval_dataset_data_amount_limit": "The evaluation dataset size has exceeded the maximum.", "code_error.system_error.license_eval_metric_amount_limit": "The number of evaluation metrics has exceeded the maximum.", - "code_error.team_error.ai_points_not_enough": "AI points are insufficient.", - "code_error.team_error.app_amount_not_enough": "The number of apps has reached the maximum.", + "code_error.team_error.ai_points_not_enough": "Credits are insufficient.", + "code_error.team_error.app_amount_not_enough": "The number of apps reaches the maximum.", "code_error.team_error.cannot_delete_default_group": "The group cannot be deleted because it is a default group.", "code_error.team_error.cannot_delete_non_empty_org": "The department cannot be deleted because it is not empty.", - "code_error.team_error.cannot_modify_root_org": "The department cannot be deleted because it is the root department.", + "code_error.team_error.cannot_modify_root_org": "The root department cannot be modified.", "code_error.team_error.cannot_move_to_sub_path": "You cannot move it to the same directory or a sub-directory.", - "code_error.team_error.dataset_amount_not_enough": "The number of knowledge bases has reached the maximum.", - "code_error.team_error.dataset_size_not_enough": "The knowledge base capacity is insufficient. Please expand it first.", + "code_error.team_error.dataset_amount_not_enough": "The number of knowledge bases reaches the maximum.", + "code_error.team_error.dataset_size_not_enough": "The knowledge base indexes areinsufficient. Please expand it first.", "code_error.team_error.group_name_duplicate": "The group name already exists.", "code_error.team_error.group_name_empty": "Group name is required.", "code_error.team_error.group_not_exist": "The group does not exist.", @@ -186,32 +186,32 @@ "code_error.team_error.org_not_exist": "The department does not exist.", "code_error.team_error.org_parent_not_exist": "The parent department does not exist.", "code_error.team_error.over_size": "The number of team members has exceeded the maximum.", - "code_error.team_error.plugin_amount_not_enough": "The number of plugins has reached the maximum.", + "code_error.team_error.plugin_amount_not_enough": "The number of plugins reaches the maximum.", "code_error.team_error.re_rank_not_enough": "Reranking of retrieved results is not supported for the free edition.", - "code_error.team_error.too_many_invitations": "The number of valid invitation links has reached the maximum. Please delete some links first.", - "code_error.team_error.un_auth": "You do not have permission to perform operations on the team.", + "code_error.team_error.too_many_invitations": "The number of valid invitation links reaches the maximum. Please delete some first.", + "code_error.team_error.un_auth": "You do not have permission on the team.", "code_error.team_error.user_not_active": "The user has not accepted the invitation or has left the team.", - "code_error.team_error.website_sync_not_enough": "The free version cannot be synchronized with the web site ~", - "code_error.team_error.you_have_been_in_the_team": "You are already in this team", + "code_error.team_error.website_sync_not_enough": "Free edition does not support website sync.", + "code_error.team_error.you_have_been_in_the_team": "You are already a member of the team.", "code_error.team_error.evaluation_task_amount_not_enough": "The number of evaluation tasks has reached the maximum.", "code_error.team_error.evaluation_dataset_amount_not_enough": "The number of evaluation datasets has reached the maximum.", - "code_error.team_error.evaluation_dataset_data_amount_not_enough": "The number of evaluation dataset data entries has reached the maximum.", + "code_error.team_error.evaluation_dataset_data_amount_not_enough": "The number of evaluation items has reached the maximum.", "code_error.team_error.evaluation_metric_amount_not_enough": "The number of evaluation metrics has reached the maximum.", - "code_error.token_error_code.403": "Invalid Login Status, Please Re-login", - "code_error.user_error.balance_not_enough": "Insufficient Account Balance", - "code_error.user_error.bin_visitor_guest": "You Are Currently a Guest, Unauthorized to Operate", + "code_error.token_error_code.403": "Invalid login status. Please log in again.", + "code_error.user_error.balance_not_enough": "The account balance is insufficient.", + "code_error.user_error.bin_visitor_guest": "You do not have permission because you have not logged in.", "code_error.user_error.un_auth_user": "User not found.", "comfirm_import": "Confirm", "comfirm_leave_page": "Are you sure you want to leave this page?", "comfirn_create": "Confirm", - "commercial_function_tip": "Please upgrade to the commercial edition to enable this feature: https://doc.fastgpt.cn/docs/introduction/commercial/", + "commercial_function_tip": "Please upgrade to the commercial edition first: https://doc.fastgpt.cn/docs/introduction/commercial/", "comon.Continue_Adding": "Save & add more", "compliance.chat": "The content is generated by a third-party AI model and is for reference only. Its authenticity or accuracy is not guaranteed.", "compliance.dataset": "Please make sure that your content is compliant with applicable laws and regulations and does not contain illegal or infringing information. Upload content that may contain sensitive information with caution.", "confirm_choice": "Confirm", "confirm_logout": "Are you sure you want to log out?", "confirm_move": "Move", - "confirm_update": "Confirm", + "confirm_update": "OK", "contact_way": "Notification recipient", "contribute_app_template": "Third-party templates", "copy_successful": "Copied successfully.", @@ -222,22 +222,22 @@ "core.ai.Not deploy rerank model": "No reranker models available.", "core.ai.Prompt": "Prompt", "core.ai.Support tool": "Function call", - "core.ai.model.Dataset Agent Model": "Text understanding model", + "core.ai.model.Dataset Agent Model": "LLM", "core.ai.model.Vector Model": "Index model", - "core.ai.model.doc_index_and_dialog": "Document index & chat index", + "core.ai.model.doc_index_and_dialog": "Document & chat indexes", "core.app.Api request": "API access", - "core.app.Api request desc": "Connect to the existing system or other platforms, such as WeCom and Feishu, using APIs.", + "core.app.Api request desc": "Connect to other platforms via APIs, such as WeCom and Feishu.", "core.app.App intro": "App description", "core.app.Auto execute": "Auto execution", "core.app.Chat Variable": "Chat box variable", - "core.app.Config schedule plan": "Click to configure scheduled execution", - "core.app.Config whisper": "Click to configure the speech-to-text feature", - "core.app.Config_auto_execute": "Click to configure auto execution rules", - "core.app.Interval timer config": "Scheduled execution configuration", - "core.app.Interval timer run": "Scheduled execution", + "core.app.Config schedule plan": "Click to configure", + "core.app.Config whisper": "Click to configure", + "core.app.Config_auto_execute": "Click to configure", + "core.app.Interval timer config": "Scheduled execution", + "core.app.Interval timer run": "Status", "core.app.Interval timer tip": "Execute apps based on the schedule.", "core.app.Make a brief introduction of your app": "Enter description for the AI app", - "core.app.Name and avatar": "Tool image & name", + "core.app.Name and avatar": "Icon & name", "core.app.Publish": "Publish", "core.app.Publish Confirm": "Are you sure you want to publish the app? The app status will be immediately updated across all publishing channels.", "core.app.Publish app tip": "The app version will be updated across all publishing channels.", @@ -246,36 +246,36 @@ "core.app.QG.Custom prompt tip2": "the prompts highlighted in yellow", "core.app.QG.Custom prompt tip3": "cannot be modified.", "core.app.QG.Fixed Prompt": "Please strictly follow the format rules: \nReturn questions in JSON format: ['Question 1', 'Question 2', 'Question 3'].", - "core.app.Question Guide": "Guess what you want", + "core.app.Question Guide": "Follow-up", "core.app.Quote prompt": "Prompt", "core.app.Quote templates": "Template", "core.app.Random": "Creative", "core.app.Search team tags": "Tag", - "core.app.Select TTS": "Select speech playback mode", + "core.app.Select TTS": "Click to configure", "core.app.Select quote template": "Select template", "core.app.Set a name for your app": "App name", - "core.app.Setting ai property": "Click to configure AI model attributes", - "core.app.Share link": "Login-free links", + "core.app.Setting ai property": "Click to configure", + "core.app.Share link": "Login-free link", "core.app.Share link desc": "Share links with other users for login-free model access.", - "core.app.Share link desc detail": "You can create login-free links for the model and share them with other users, who can use the links to chat with the model without logging in. Note: This feature will consume your AI points. Please keep the links properly.", - "core.app.TTS": "Speech playback", - "core.app.TTS Tip": "If enabled, the content of each chat can be played back as audio. Using this feature may generate additional costs.", + "core.app.Share link desc detail": "You can create login-free links to the model and share them with other users, who can then chat with the model via the links, without login required. Note: Using this feature will consume your credits. Please keep the links properly.", + "core.app.TTS": "Text-to-speech", + "core.app.TTS Tip": "If enabled, the content of each chat reply can be played as audio. Using this feature may generate additional costs.", "core.app.TTS start": "Read content", "core.app.Team tags": "Team tag", "core.app.Tool call": "Tool call", "core.app.ToolCall.No plugin": "No plugins available.", - "core.app.ToolCall.Parameter setting": "Enter parameter", + "core.app.ToolCall.Parameter setting": "Input parameter", "core.app.ToolCall.System": "System", "core.app.ToolCall.Team": "Team", "core.app.Welcome Text": "Conversation opener", "core.app.Whisper": "Speech-to-text", - "core.app.Whisper config": "Speech-to-text configuration", + "core.app.Whisper config": "Configure speech-to-text", "core.app.deterministic": "Deterministic", "core.app.edit.Prompt Editor": "Edit prompt", - "core.app.edit.Query extension background prompt": "Chat background description", - "core.app.edit.Query extension background tip": "Specify the background of the current chat to help the model complete and expand the information of the current question. The specified content is typically used by the app.", + "core.app.edit.Query extension background prompt": "Chat background", + "core.app.edit.Query extension background tip": "Describe the background of the current chat to help the model supplement and elaborate on the current question. The specified content is typically used by the app.", "core.app.edit_content": "Edit app", - "core.app.error.App name can not be empty": "App name is required.", + "core.app.error.App name can not be empty": "The app name is required.", "core.app.error.Get app failed": "Error occurred while obtaining the app.", "core.app.feedback.Custom feedback": "Custom feedback", "core.app.feedback.close custom feedback": "Disable feedback", @@ -291,19 +291,19 @@ "core.app.outLink.Script Close Icon": "Hide icon", "core.app.outLink.Script Open Icon": "Display icon", "core.app.outLink.Script block title": "Add the following code to your website.", - "core.app.outLink.Select Mode": "Use now", + "core.app.outLink.Select Mode": "Details", "core.app.outLink.Select Using Way": "Mode", "core.app.outLink.Show History": "Display chat history", "core.app.publish.Fei shu bot": "Feishu", - "core.app.publish.Fei shu bot publish": "Publish app to Feishu", + "core.app.publish.Fei shu bot publish": "Feishu bots", "core.app.schedule.Default prompt": "Default question", - "core.app.schedule.Default prompt placeholder": "A default question used when the app is executed", + "core.app.schedule.Default prompt placeholder": "Default question used when the app is executed", "core.app.schedule.Every day": "Every day at {{hour}}:00", "core.app.schedule.Every month": "Day {{day}} of each month at {{hour}}:00", "core.app.schedule.Every week": "Every {{day}} at {{hour}}:00", "core.app.schedule.Interval": "Every {{interval}} hours", "core.app.schedule.Open schedule": "Scheduled execution", - "core.app.setting": "Configure App info", + "core.app.setting": "Configure app info", "core.app.share.Amount limit tip": "Up to 10 groups can be created.", "core.app.share.Create link": "Create link", "core.app.share.Create link tip": "Created successfully. The link has been copied and can be shared now.", @@ -312,11 +312,11 @@ "core.app.share.Not share link": "No sharing links available.", "core.app.share.Role check": "Identity verification", "core.app.switch_to_template_market": "Go to Templates", - "core.app.tip.Add a intro to app": "You can enter a description for this app.", - "core.app.tip.chatNodeSystemPromptTip": "Enter a prompt here", - "core.app.tip.systemPromptTip": "Predefined prompt for the model. You can change the subject of a chat with the model by adjusting the prompt. The content will be placed at the beginning of the context all the time. You can insert a variable by entering a slash (/).\nIf a knowledge base is associated, you can specify when the model performs a knowledge base search by using appropriate descriptions. Example:\nYou are an assistant for the movie Interstellar. When a user asks about Interstellar, search the knowledge base and incorporate the results in your answer.", - "core.app.tip.variableTip": "Before a chat begins, you can require the user to enter content as variables for the chat. This module will be executed after the conversation opener is executed.\nUsers can enter a slash (/) in the input field to enable selectable variables such as prompts or qualifiers.", - "core.app.tip.welcomeTextTip": "Predefined content sent before a chat begins. Supports standard Markdown syntax and the following marks:\n[ ]: Put the conversation opener in a pair of brackets to create a shortcut.", + "core.app.tip.Add a intro to app": "Describe this app...", + "core.app.tip.chatNodeSystemPromptTip": "", + "core.app.tip.systemPromptTip": "Predefined prompt for the model. You can adjust it to change subjects for chats using the model. The content will be placed at the beginning of the context. You can insert a variable by entering a slash (/).\nIf a knowledge base is associated, you can instruct the model when to trigger knowledge base searches using appropriate descriptions. Example:\nYou are an assistant for the movie Interstellar. When a user asks about Interstellar, search the knowledge base and incorporate the information into your answers.", + "core.app.tip.variableTip": "Before a chat begins, you can prompt users to provide information that will serve as variables for the chat. This module runs after the conversation opener.\nUsers can enter a slash (/) in the input box to select variables such as prompts or qualifiers.", + "core.app.tip.welcomeTextTip": "Predefined content sent before a chat begins. Supports standard Markdown syntax and the following:\n[ ]: Put the conversation opener in a pair of brackets to create a shortcut.", "core.app.tool_label.doc": "Documentation", "core.app.tool_label.github": "GitHub address", "core.app.tool_label.price": "Billing", @@ -324,24 +324,24 @@ "core.app.tts.Speech model": "Text-to-speech model", "core.app.tts.Speech speed": "Speed", "core.app.tts.Test Listen": "Test", - "core.app.tts.Test Listen Text": "Hello, this is a speech test. If you can hear this sentence, speech playback works properly.", + "core.app.tts.Test Listen Text": "Hello, this is a speech test. If you can hear this sentence, text-to-speech works properly.", "core.app.whisper.Auto send": "Auto send", - "core.app.whisper.Auto send tip": "Text converted from speech will be sent automatically after it is complete, without the need to click Send.", + "core.app.whisper.Auto send tip": "A text message will be sent automatically after speech-to-text is complete, without the need to click Send.", "core.app.whisper.Auto tts response": "Auto speech reply", - "core.app.whisper.Auto tts response tip": "A speech reply will be automatically triggered for a speech input. Please make sure that speech playback is enabled.", + "core.app.whisper.Auto tts response tip": "A speech reply will be automatically triggered for a speech-to-text message. Please ensure that text-to-speech is enabled.", "core.app.whisper.Close": "Disable", - "core.app.whisper.Not tts tip": "The feature is unavailable because speech playback is not enabled.", + "core.app.whisper.Not tts tip": "It is unavailable because text-to-speech is not enabled.", "core.app.whisper.Open": "Enable", - "core.app.whisper.Switch": "Enable speech-to-text conversion", + "core.app.whisper.Switch": "Speech-to-text", "core.chat.Admin Mark Content": "Corrected answer", - "core.chat.Audio Not Support": "The device does not support speech playback.", - "core.chat.Audio Speech Error": "Speech playback error occurred.", - "core.chat.Cancel Speak": "Cancel speech-to-text conversion", - "core.chat.Confirm to clear history": "Are you sure you want to clear the online chat records of the app? Shared content and API call records will not be cleared.", - "core.chat.Confirm to clear share chat history": "Are you sure you want to delete all chat records?", + "core.chat.Audio Not Support": "The device does not support text-to-speech.", + "core.chat.Audio Speech Error": "Text-to-speech error.", + "core.chat.Cancel Speak": "Cancel speech-to-text", + "core.chat.Confirm to clear history": "Are you sure you want to clear the online chat history of the app? Shared content and API calls will be retained.", + "core.chat.Confirm to clear share chat history": "Are you sure you want to delete all chat history?", "core.chat.Converting to text": "Converting to text...", - "core.chat.Custom History Title": "Custom history title", - "core.chat.Custom History Title Description": "If not configured, the title will be automatically generated based on the chat record.", + "core.chat.Custom History Title": "Custom title", + "core.chat.Custom History Title Description": "If not configured, a title will be automatically generated based on the chat history.", "core.chat.Exit Chat": "Exit", "core.chat.Failed to initialize chat": "Chat initialization failed.", "core.chat.Feedback Failed": "Error occurred while submitting feedback.", @@ -349,45 +349,45 @@ "core.chat.Feedback Modal Tip": "What you are dissatisfied with", "core.chat.Feedback Submit": "Submit", "core.chat.Feedback Success": "Feedback submitted successfully.", - "core.chat.Finish Speak": "Speech-to-text conversion successful.", + "core.chat.Finish Speak": "Complete speech-to-text", "core.chat.History": "Records", "core.chat.History Amount": "{{amount}} records", "core.chat.Mark": "Mark as expected answer", - "core.chat.Mark Description": "The mark feature is currently in beta test.\n\nAfter a mark is added, you need to select a knowledge base to store the marked entry. This feature allows you to quickly mark questions and expected answers to guide the future responses of the model.\n\nAnswers marked as expected will be stored in the knowledge base but will not necessarily be retrieved when users ask related questions.\n\nMarked entries are synced to the selected knowledge bases, but changes to the marked entries in knowledge bases are not synced to the corresponding logs.", + "core.chat.Mark Description": "The mark feature is currently in test mode.\n\nAfter clicking the mark icon, you need to select a knowledge base to store the marked entry. This feature allows you to quickly mark questions and expected answers to guide future responses from the model.\n\nAnswers marked as expected will be stored in the knowledge base but will not necessarily be retrieved when users ask related questions.\n\nMarked entries are synced to the selected knowledge bases, in which changes to marked entries are not synced to corresponding logs.", "core.chat.Mark Description Title": "Marking Function Introduction", "core.chat.New Chat": "New chat", - "core.chat.Pin": "Move to top", - "core.chat.Question Guide": "Guess what you want", + "core.chat.Pin": "Pin to top", + "core.chat.Question Guide": "Follow-up", "core.chat.Quote": "Reference", "core.chat.Quote Amount": "Knowledge base references ({{amount}})", "core.chat.Read Mark Description": "View Marking Function Introduction", - "core.chat.Recent use": "Last used", + "core.chat.Recent use": "Recents", "core.chat.Record": "Speech-to-text", - "core.chat.Restart": "Start new chat", + "core.chat.Restart": "Start a new chat", "core.chat.Run test": "Running preview", "core.chat.Select dataset": "Select knowledge base", - "core.chat.Select dataset Desc": "Select a knowledge base for storing marked answers.", + "core.chat.Select dataset Desc": "Select a knowledge base to store marked answers.", "core.chat.Send Message": "Send", - "core.chat.Speaking": "I'm listening. Please speak.", - "core.chat.Type a message": "Enter your question, and press Enter to send it or press Ctrl/Alt/Shift and Enter to start a new line", + "core.chat.Speaking": "I'm listening. Go ahead.", + "core.chat.Type a message": "Enter your question. Press Enter to send or Ctrl/Alt/Shift+Enter for a new line.", "core.chat.Unpin": "Remove from top", - "core.chat.error.Chat error": "Chat encountered error.", - "core.chat.error.Messages empty": "The API returned empty content, possibly because the entered text length exceeded the maximum.", - "core.chat.error.Select dataset empty": "Please select at least one knowledge base.", - "core.chat.error.User input empty": "The entered question is empty.", + "core.chat.error.Chat error": "Chat error.", + "core.chat.error.Messages empty": "Empty response. The request text length exceeds the maximum.", + "core.chat.error.Select dataset empty": "Please select a knowledge base.", + "core.chat.error.User input empty": "The question cannot be empty.", "core.chat.error.data_error": "Error occurred while obtaining data.", - "core.chat.feedback.Close User Like": "Liked by user.\nClick to disable the icon.", + "core.chat.feedback.Close User Like": "Liked by user.\nClick to dislike it.", "core.chat.feedback.Feedback Close": "Disable feedback", - "core.chat.feedback.No Content": "The user did not enter specific feedback.", + "core.chat.feedback.No Content": "The user did not provide any specific feedback.", "core.chat.feedback.Read User dislike": "Disliked by user.\nClick to view the content.", "core.chat.logs.api": "API call", "core.chat.logs.feishu": "Feishu", - "core.chat.logs.free_login": "Login-free links", + "core.chat.logs.free_login": "Login-free link", "core.chat.logs.mcp": "MCP call", "core.chat.logs.official_account": "WeChat official account", "core.chat.logs.online": "Online operation", "core.chat.logs.share": "External link call", - "core.chat.logs.team": "Chat among team members", + "core.chat.logs.team": "Chat with team members", "core.chat.logs.test": "Online debugging", "core.chat.logs.wecom": "WeCom", "core.chat.markdown.Edit Question": "Edit question", @@ -401,7 +401,7 @@ "core.chat.response.Complete Response": "Complete response", "core.chat.response.Extension model": "Question optimization model", "core.chat.response.Read complete response": "View details", - "core.chat.response.Read complete response tips": "Click to view the detailed process", + "core.chat.response.Read complete response tips": "Click to view the process details.", "core.chat.response.Tool call input tokens": "Input tokens for tool call", "core.chat.response.Tool call output tokens": "Output tokens for tool call", "core.chat.response.Tool call tokens": "Tokens consumed for tool call", @@ -414,9 +414,9 @@ "core.chat.response.module cq result": "Classification result", "core.chat.response.module extract description": "Extract background description", "core.chat.response.module extract result": "Extraction result", - "core.chat.response.module historyPreview": "Preview (Only some records are displayed.)", + "core.chat.response.module historyPreview": "Preview (partially displayed)", "core.chat.response.module http result": "Response body", - "core.chat.response.module if else Result": "Judger running result", + "core.chat.response.module if else Result": "If-else running result", "core.chat.response.module limit": "Max entries per search", "core.chat.response.module maxToken": "Max output tokens", "core.chat.response.module model": "Model", @@ -428,30 +428,30 @@ "core.chat.response.plugin output": "Plugin output value", "core.chat.response.search using reRank": "Result reranking", "core.chat.response.text output": "Text output", - "core.chat.response.update_var_result": "Variable update results (multiple variable update results will be displayed in sequence)", - "core.chat.response.user_select_result": "Selected variables", + "core.chat.response.update_var_result": "Variable update results (multiple variable updates displayed in sequence)", + "core.chat.response.user_select_result": "Selection result", "core.chat.retry": "Generate again", "core.chat.tts.Stop Speech": "Stop", - "core.dataset.Choose Dataset": "Associated knowledge bases", + "core.dataset.Choose Dataset": "Knowledge bases", "core.dataset.Collection": "Dataset", "core.dataset.Create dataset": "Create {{name}}", "core.dataset.Dataset": "Knowledge base", "core.dataset.Dataset ID": "Knowledge base ID", - "core.dataset.Delete Confirm": "Are you sure you want to delete the knowledge base? The data cannot be recovered.", + "core.dataset.Delete Confirm": "Are you sure you want to delete the knowledge base? Deleted data cannot be recovered.", "core.dataset.Empty Dataset": "Empty dataset", "core.dataset.Empty Dataset Tips": "No knowledge bases available. Please create one first.", "core.dataset.Folder placeholder": "This is a directory.", - "core.dataset.Intro Placeholder": "No description available.", + "core.dataset.Intro Placeholder": "No description yet.", "core.dataset.My Dataset": "My knowledge bases", - "core.dataset.Query extension intro": "Enabling this feature can increase the accuracy of knowledge base searches during continuous chats and allow the model to complete questions based on chat records.", + "core.dataset.Query extension intro": "Enabling it will increase the accuracy of knowledge base searches during continuous chats and allow the model to complete questions based on chat history.", "core.dataset.Quote Length": "Referenced content length", "core.dataset.Read Dataset": "View details", - "core.dataset.Set Website Config": "Confiugre now", - "core.dataset.Start export": "Export...", + "core.dataset.Set Website Config": "Configure now", + "core.dataset.Start export": "Exporting...", "core.dataset.Text collection": "Text dataset", "core.dataset.apiFile": "API file", "core.dataset.collection.Click top config website": "Configure website", - "core.dataset.collection.Collection raw text": "Dataset entries", + "core.dataset.collection.Collection raw text": "Dataset content", "core.dataset.collection.Empty Tip": "The dataset is empty.", "core.dataset.collection.QA Prompt": "Prompt for Q&A generation", "core.dataset.collection.Start Sync Tip": "Are you sure you want to sync data? The original data will be deleted.", @@ -469,7 +469,7 @@ "core.dataset.collection.metadata.source": "Data source", "core.dataset.collection.metadata.source size": "Source data size", "core.dataset.collection.status.active": "Ready", - "core.dataset.collection.status.error": "Training error occurred.", + "core.dataset.collection.status.error": "Training error.", "core.dataset.collection.sync.result.sameRaw": "Update is not required because the content is not changed.", "core.dataset.collection.sync.result.success": "Sync now", "core.dataset.data.Data Content": "Related data", @@ -477,13 +477,13 @@ "core.dataset.data.Edit": "Edit data", "core.dataset.data.Empty Tip": "No data available in the dataset.", "core.dataset.data.Search data placeholder": "Related data", - "core.dataset.data.Too Long": "The length has exceeded the maximum.", + "core.dataset.data.Too Long": "The length exceeds the maximum.", "core.dataset.data.Updated": "Updated", "core.dataset.data.group": "groups", "core.dataset.data.unit": "Items", - "core.dataset.embedding model tip": "An index model can convert natural language to vectors for semantic search.\nNote: Different index models cannot be used at the same time. The index model cannot be changed after selection.", + "core.dataset.embedding model tip": "An index model can convert natural language to vectors for semantic search.\nNote: Different index models cannot be used together. The index model cannot be changed after being selected.", "core.dataset.error.Data not found": "The entry does not exist or has been deleted.", - "core.dataset.error.Start Sync Failed": "Failed to start syncing.", + "core.dataset.error.Start Sync Failed": "Failed to start sync.", "core.dataset.error.canNotEditAdminPermission": "Admin permissions cannot be modified.", "core.dataset.error.invalidVectorModelOrQAModel": "Vector model or Q&A model encountered error.", "core.dataset.error.unAuthDataset": "You do not have permission to perform operations on the knowledge base.", @@ -492,7 +492,7 @@ "core.dataset.error.unAuthDatasetFile": "You do not have permission to perform operations on the file.", "core.dataset.error.unCreateCollection": "You do not have permission to perform operations on the entry.", "core.dataset.error.unExistDataset": "The knowledge base does not exist.", - "core.dataset.error.unLinkCollection": "Must be a link set.", + "core.dataset.error.unLinkCollection": "It is not a link set.", "core.dataset.externalFile": "External file database", "core.dataset.file": "File", "core.dataset.folder": "Directory", @@ -501,44 +501,44 @@ "core.dataset.import.Continue upload": "Continue", "core.dataset.import.Custom prompt": "Custom prompt", "core.dataset.import.Custom text": "Custom text", - "core.dataset.import.Custom text desc": "Text manually entered as a dataset", + "core.dataset.import.Custom text desc": "Manually enter text to create a dataset.", "core.dataset.import.Data process params": "Data processing parameter", - "core.dataset.import.Down load csv template": "Download CSV template", + "core.dataset.import.Down load csv template": "Download .csv template", "core.dataset.import.Link name": "Link", - "core.dataset.import.Link name placeholder": "Must be a static link. If no data is available after the link is specified, the link may fail to be read.\nOne link per line. Up to 10 links are allowed.", + "core.dataset.import.Link name placeholder": "Must be static links. If no data is available after the upload, the links may fail to be read.\nOne link per line. Up to 10 links are allowed.", "core.dataset.import.Local file": "Local file", - "core.dataset.import.Local file desc": "Upload files in PDF, TXT, DOCX, or other formats", + "core.dataset.import.Local file desc": "Upload files in .pdf, .txt, .docx, or other formats.", "core.dataset.import.Preview chunks": "Preview shard (Max shards: 15)", - "core.dataset.import.Preview raw text": "Preview source text (Max characters: 3000)", + "core.dataset.import.Preview raw text": "Preview source text (Max characters: 3000)", "core.dataset.import.Process way": "Processing Method", "core.dataset.import.QA Import": "Q&A generation", - "core.dataset.import.QA Import Tip": "Split the text into multiple large paragraphs based on specific rules and generate Q&A pair for them using the model. This provides extremely high search accuracy but can cause detail loss.", + "core.dataset.import.QA Import Tip": "Split the text into multiple large paragraphs based on specific rules and generate Q&A pairs for them using the model. This provides extremely high search accuracy but can cause significant detail loss.", "core.dataset.import.Select file": "Select File", "core.dataset.import.Select source": "Select source", "core.dataset.import.Source name": "Source name", "core.dataset.import.Sources list": "Sources", "core.dataset.import.Start upload": "Upload", - "core.dataset.import.Upload complete": "Upload completed", - "core.dataset.import.Upload data": "Upload file", + "core.dataset.import.Upload complete": "Complete upload", + "core.dataset.import.Upload data": "Confirm", "core.dataset.import.Upload file progress": "File upload progress", "core.dataset.import.Upload status": "Status", "core.dataset.import.Web link": "Web link", - "core.dataset.import.Web link desc": "Content read from static webpages as a dataset", + "core.dataset.import.Web link desc": "Extract content from static webpages to create a dataset.", "core.dataset.import.import_success": "Import successful. Please wait for training.", "core.dataset.link": "Link", - "core.dataset.search.Dataset Search Params": "Knowledge base search configuration", - "core.dataset.search.Empty result response": "Answer for no search result", + "core.dataset.search.Dataset Search Params": "Knowledge base search settings", + "core.dataset.search.Empty result response": "Answer for no search results", "core.dataset.search.Filter": "Filtering", "core.dataset.search.No support similarity": "Relevance filtering is supported only for result reranking or semantic search.", "core.dataset.search.Nonsupport": "Not supported", - "core.dataset.search.Params Setting": "Parameter configuration", + "core.dataset.search.Params Setting": "Parameter settings", "core.dataset.search.Quote index": "No.", "core.dataset.search.ReRank": "Result reranking", - "core.dataset.search.ReRank desc": "Rerank results using the reranker model to improve overall rankings.", + "core.dataset.search.ReRank desc": "Rerank results using the rerank model to improve overall rankings.", "core.dataset.search.Source id": "Source ID", "core.dataset.search.Source index": "What sources", "core.dataset.search.Source name": "Reference source name", - "core.dataset.search.Using query extension": "Enable question optimization", + "core.dataset.search.Using query extension": "Enable", "core.dataset.search.mode.embedding": "Semantic search", "core.dataset.search.mode.embedding desc": "Queries text relevance using vectors.", "core.dataset.search.mode.fullTextRecall": "Full-text search", @@ -547,9 +547,9 @@ "core.dataset.search.mode.mixedRecall desc": "Combines vector search and full-text search. The returned entries are ranked using the RRF algorithm.", "core.dataset.search.score.embedding desc": "The vector search score is obtained by calculating the distances between vectors. Range: 0-1", "core.dataset.search.score.fullText": "Full-text search", - "core.dataset.search.score.fullText desc": "The full-text search score is obtained by calculating occurrences of identical keywords. Range: 0 to infinity.", + "core.dataset.search.score.fullText desc": "The full-text search score is obtained by calculating occurrences of identical keywords. Range: 0 to infinity", "core.dataset.search.score.reRank": "Result reranking", - "core.dataset.search.score.reRank desc": "The relevance between sentences is calculated by using the reranker model. Range: 0-1", + "core.dataset.search.score.reRank desc": "Relevance between sentences calculated using the reranker model. Range: 0-1", "core.dataset.search.score.rrf": "Overall ranking", "core.dataset.search.score.rrf desc": "Multiple search results are merged using inverted ranking.", "core.dataset.search.search mode": "Search method", @@ -557,33 +557,33 @@ "core.dataset.status.syncing": "Syncing", "core.dataset.status.waiting": "Waiting", "core.dataset.test.Batch test": "Bulk test", - "core.dataset.test.Batch test Placeholder": "Please select a CSV file.", - "core.dataset.test.Search Test": "Test", + "core.dataset.test.Batch test Placeholder": "Please select a .csv file.", + "core.dataset.test.Search Test": "Search test", "core.dataset.test.Test": "Test", "core.dataset.test.Test Result": "Test result", "core.dataset.test.Test Text": "Text test", - "core.dataset.test.Test Text Placeholder": "Text to be tested", + "core.dataset.test.Test Text Placeholder": "Enter text for testing", "core.dataset.test.Test params": "Test parameter", "core.dataset.test.delete test history": "Delete test result", - "core.dataset.test.test history": "Test History", + "core.dataset.test.test history": "Test history", "core.dataset.test.test result placeholder": "The test result will be displayed here.", - "core.dataset.test.test result tip": "The returned results are ranked based on the similarity between the knowledge base entries and the tested text. You can adjust the corresponding text based on test results.\nNote: Entries in the test record may have been modified. To view the latest entry, click the tested entry.", + "core.dataset.test.test result tip": "The returned results are sorted based on the similarity between the knowledge base entries and the tested text. You can adjust the corresponding text based on test results.\nNote: Entries in the test history may have been modified. Click to view the latest entry.", "core.dataset.training.Agent queue": "Q&A training queue", "core.dataset.training.Auto mode": "Supplemental index", - "core.dataset.training.Auto mode Tip": "Related questions and summaries will be generated using sub-indexes and the model, which improves the semantic richness of data blocks and enhances search efficiency. However, more storage space and model calls are required.", + "core.dataset.training.Auto mode Tip": "Related questions and summaries will be generated using sub-indexes and model calls, which improves the semantic richness of data blocks and enhances search efficiency. However, more storage space and model calls are required.", "core.dataset.training.Chunk mode": "Chunking", "core.dataset.training.Full": "Estimated wait time: Over 20 minutes", "core.dataset.training.Leisure": "Idle", "core.dataset.training.QA mode": "Q&A pair extraction", - "core.dataset.training.Vector queue": "Indexing queue", + "core.dataset.training.Vector queue": "Index queue", "core.dataset.training.Waiting": "Estimated wait time: 20 minutes", "core.dataset.training.Website Sync": "Website sync", "core.dataset.training.tag": "Queue status", "core.dataset.website.Base Url": "Root address", "core.dataset.website.Config": "Website configuration", - "core.dataset.website.Config Description": "If the web sync feature is enabled, you can specify the root address of a website, and the system will automatically capture relevant webpages for knowledge base training. Only static webpages will be captured, mainly including project documents and blogs.", + "core.dataset.website.Config Description": "If the website sync feature is enabled, you can specify the root address of a website, and the system will automatically capture relevant webpages for knowledge base training. Only static webpages will be captured, mainly including project documents and blogs.", "core.dataset.website.Confirm Create Tips": "The sync task will start immediately.", - "core.dataset.website.Confirm Update Tips": "Are you sure you want to update the site configuration? The new configuration will be applied immediately.", + "core.dataset.website.Confirm Update Tips": "Are you sure you want to update the website configuration?", "core.dataset.website.Selector": "Selector", "core.dataset.website.Selector Course": "Guide", "core.dataset.website.Start Sync": "Sync now", @@ -597,7 +597,7 @@ "core.module.Default Value": "Default value", "core.module.Default value": "Default value", "core.module.Default value placeholder": "If left blank, an empty string will be returned by default.", - "core.module.Diagram": "Illustration", + "core.module.Diagram": "Preview", "core.module.Edit intro": "Edit description", "core.module.Field Description": "Field description", "core.module.Field Name": "Field name", @@ -615,10 +615,10 @@ "core.module.Setting quote prompt": "Configure prompt", "core.module.Variable": "Global variables", "core.module.Variable Setting": "Configure variable", - "core.module.edit.Field Name Cannot Be Empty": "Field name is required.", - "core.module.edit.Field Value Type Cannot Be Empty": "Data type is required.", + "core.module.edit.Field Name Cannot Be Empty": "The field name is required.", + "core.module.edit.Field Value Type Cannot Be Empty": "The data type is required.", "core.module.extract.Add field": "Add field", - "core.module.extract.Enum Description": "Valid values for the field. One value per line.", + "core.module.extract.Enum Description": "Enter the field values. One value per line.", "core.module.extract.Enum Value": "Enumeration values", "core.module.extract.Field Description Placeholder": "Name, age, SQL statement, etc.", "core.module.extract.Field Setting Title": "Extracted field configuration", @@ -631,27 +631,27 @@ "core.module.http.Current time": "Current time", "core.module.http.Histories": "History", "core.module.http.Key already exists": "The key already exists.", - "core.module.http.Key cannot be empty": "Parameter name is required.", + "core.module.http.Key cannot be empty": "The parameter name is required.", "core.module.http.Props name": "Parameter name", - "core.module.http.Props tip": "HTTP request parameters can be configured.\nYou can call a variable by entering a slash (/). Available variables:\n{{variable}}", + "core.module.http.Props tip": "Configure HTTP request parameters.\nYou can enter a slash (/) to call a variable. Available variables:\n{{variable}}", "core.module.http.Props value": "Parameter value", "core.module.http.ResponseChatItemId": "Answer ID", "core.module.http.Url and params have been split": "Path parameters have been automatically added to Params", - "core.module.http.curl import": "Import using CURL", - "core.module.http.curl import placeholder": "Please enter the CURL command. The system will extract the request information from the first interface.", + "core.module.http.curl import": "Import using cURL", + "core.module.http.curl import placeholder": "Please enter a cURL command. The system will extract the request details from the first API.", "core.module.input.Add Branch": "Add branch", "core.module.input.add": "Add condition", - "core.module.input.description.Background": "You can add descriptions for specific content to help the system identify the question type. The descriptions are typically used to introduce the model to unfamiliar content.", - "core.module.input.description.HTTP Dynamic Input": "Output values from frontend nodes are received as variables, which can be used in HTTP request parameters.", - "core.module.input.description.Http Request Header": "Custom request header. Enter a JSON string that meets the following requirements.\n1. The last attribute cannot contain commas.\n2. The key must be enclosed in double quotes.\nExample: {\"Authorization\":\"Bearer xxx\"}", + "core.module.input.description.Background": "You can add descriptions for specific content to help the system identify the question type. The descriptions are typically used to introduce unfamiliar content to the model.", + "core.module.input.description.HTTP Dynamic Input": "Output values from upstream nodes are received as variables, which can be used in HTTP request parameters.", + "core.module.input.description.Http Request Header": "Custom request header. Enter a JSON string that meets the following requirements:\n1. The last attribute is not followed by a comma.\n2. The key must be enclosed in double quotes.\nExample: {\"Authorization\":\"Bearer xxx\"}", "core.module.input.description.Http Request Url": "New HTTP request address. If two request addresses exist, delete the module and add it again to obtain the latest module configuration.", - "core.module.input.description.Response content": "Consecutive line breaks can be implemented by using \\n.\nThe answer can be entered by using an external module. Content entered using the external module will overwrite the existing content.\nIf the specified entry is not a string, it will be automatically converted to a string.", + "core.module.input.description.Response content": "Consecutive line breaks can be implemented using \\n.\nThe answer can be provided by an external module, which will overwrite the content specified here.\nIf the specified content is not a string, it will be automatically converted to a string.", "core.module.input.label.Background": "Background knowledge", "core.module.input.label.Http Request Url": "Request URL", "core.module.input.label.Response content": "Answer", "core.module.input.label.Select dataset": "Select knowledge base", "core.module.input.label.aiModel": "AI model", - "core.module.input.label.chat history": "Chat records", + "core.module.input.label.chat history": "Chat history", "core.module.input.label.user question": "Question", "core.module.input.placeholder.Classify background": "Example:\n1. AI-generated content (AIGC) refers to the use of artificial intelligence technologies to automatically or semi-automatically generate digital content, such as text, images, music, and videos.\n2. AIGC technologies include but are not limited to natural language processing, computer vision, machine learning, and deep learning. These technologies can create new content or modify existing content to meet specific creative, educational, entertainment, or informational needs.", "core.module.input_description": "Description", @@ -660,11 +660,11 @@ "core.module.input_type": "Type", "core.module.laf.Select laf function": "LAF function", "core.module.output.description.Ai response content": "Triggered after the stream answer is complete.", - "core.module.output.description.New context": "Combines the current answer with chat records to output a new context.", - "core.module.output.description.query extension result": "Outputs a string array that can be referenced in the Question section on the Knowledge base search page. It is not recommended to reference in the Question section on the AI chat page.", + "core.module.output.description.New context": "Combines the current answer with the chat history to output a new context.", + "core.module.output.description.query extension result": "Outputs a string array that can be referenced in the Question area on the Knowledge base search page. It is not recommended to reference in the Question area on the AI chat page.", "core.module.output.label.Ai response content": "AI answer", "core.module.output.label.New context": "New context", - "core.module.output.label.query extension result": "Optimized result", + "core.module.output.label.query extension result": "Optimization result", "core.module.template.AI function": "AI capability", "core.module.template.AI response switch tip": "If you want the current node not to output content, you can turn off this switch. The content output by AI will not be displayed to the user, and you can manually use 'AI Response Content' for special processing.", "core.module.template.AI support tool tip": "Models that support function calls can call tools more efficiently.", @@ -677,38 +677,38 @@ "core.module.template.ai_chat": "AI chat", "core.module.template.ai_chat_intro": "Chat with an AI model.", "core.module.template.all_team_app": "All", - "core.module.template.config_params": "System parameters of the app can be configured.", + "core.module.template.config_params": "Configure system parameters for the app.", "core.module.template.empty_plugin": "Blank plugin", "core.module.template.empty_workflow": "Blank workflow", "core.module.template.self_input": "Plugin input", "core.module.template.self_output": "Plugin output", - "core.module.template.system_config": "System", - "core.module.template.system_config_info": "System parameters of the app can be configured.", - "core.module.template.work_start": "Process startup", + "core.module.template.system_config": "System settings", + "core.module.template.system_config_info": "Configure system parameters for the app.", + "core.module.template.work_start": "Workflow startup", "core.module.templates.Load plugin error": "Failed to load the plugin.", "core.module.variable add option": "Add option", - "core.module.variable.Custom type": "Custom variables", + "core.module.variable.Custom type": "Custom variable", "core.module.variable.add option": "Add option", "core.module.variable.input type": "Text", "core.module.variable.key": "Variable key", - "core.module.variable.key is required": "Variable key is required.", + "core.module.variable.key is required": "The variable key is required.", "core.module.variable.select type": "Dropdown Single Select", - "core.module.variable.text max length": "Max Length", + "core.module.variable.text max length": "Max length", "core.module.variable.textarea type": "Paragraph", - "core.module.variable.variable name is required": "Variable name is required.", + "core.module.variable.variable name is required": "The variable name is required.", "core.module.variable.variable option is required": "This field is required.", "core.module.variable.variable option is value is required": "This field is required.", "core.module.variable.variable options": "Option", "core.plugin.Custom headers": "Custom request header", "core.plugin.Get Plugin Module Detail Failed": "Error occurred while loading the plugin.", "core.plugin.Http plugin intro placeholder": "For display only.", - "core.plugin.cost": "Points consumed:", + "core.plugin.cost": "Credits consumed:", "core.tip.leave page": "Leaving the page will discard current changes. Would you like to proceed?", "core.view_chat_detail": "View chat details", "core.workflow.Can not delete node": "Unable to delete the node.", - "core.workflow.Change input type tip": "Changing the input type will clear the specified values. Would you like to proceed?", + "core.workflow.Change input type tip": "Changing the input type will clear specified values. Would you like to proceed?", "core.workflow.Check Failed": "Failed to verify the workflow. Please check whether the required parameters or values are missing, and check the connection status.", - "core.workflow.Confirm stop debug": "Are you sure you want to stop debugging? The debugging information will be deleted.", + "core.workflow.Confirm stop debug": "Are you sure you want to stop debugging? The debugging information will not be retained.", "core.workflow.Copy node": "Node copied successfully.", "core.workflow.Custom inputs": "Custom input", "core.workflow.Custom outputs": "Custom output", @@ -716,10 +716,10 @@ "core.workflow.Debug": "Debug", "core.workflow.Debug Node": "Debugging mode", "core.workflow.Failed": "Failed to run the app.", - "core.workflow.Not intro": "No description available.", + "core.workflow.Not intro": "No description yet.", "core.workflow.Run": "Run", "core.workflow.Running": "Running", - "core.workflow.Save and publish": "Save and publish", + "core.workflow.Save and publish": "Save & publish", "core.workflow.Save to cloud": "Save", "core.workflow.Skipped": "Skip running", "core.workflow.Stop debug": "Stop debugging", @@ -727,7 +727,7 @@ "core.workflow.Value type": "Data type", "core.workflow.debug.Done": "Debugging completed", "core.workflow.debug.Hide result": "Hide result", - "core.workflow.debug.Not result": "No result available.", + "core.workflow.debug.Not result": "No results available.", "core.workflow.debug.Run result": "Running result", "core.workflow.debug.Show result": "Display result", "core.workflow.dynamic_input": "Dynamic input", @@ -735,11 +735,11 @@ "core.workflow.inputType.Manual input": "Manually input", "core.workflow.inputType.Manual select": "Manually select", "core.workflow.inputType.Reference": "Variable reference", - "core.workflow.inputType.custom": "Custom variables", + "core.workflow.inputType.custom": "Custom variable", "core.workflow.inputType.dynamicTargetInput": "Dynamic external data", "core.workflow.inputType.input": "Single-line input box", "core.workflow.inputType.number input": "Numeric input box", - "core.workflow.inputType.select": "Radio button", + "core.workflow.inputType.select": "Single-select dropdown", "core.workflow.inputType.selectApp": "Select app", "core.workflow.inputType.selectDataset": "Select knowledge base", "core.workflow.inputType.selectLLMModel": "Chat model", @@ -763,7 +763,7 @@ "data_index_custom": "Custom index", "data_index_default": "Default index", "data_index_question": "Predicted question index", - "data_index_summary": "Abstract index", + "data_index_summary": "Summary index", "data_not_found": "Data not found.", "dataset.Confirm move the folder": "Are you sure you want to move it to the directory?", "dataset.Confirm to delete the data": "Are you sure you want to delete the entry?", @@ -772,7 +772,7 @@ "dataset.Create manual collection": "Create manual dataset", "dataset.Delete Dataset Error": "Error occurred while deleting the knowledge base.", "dataset.Edit Folder": "Edit folder", - "dataset.Edit Info": "Edit info", + "dataset.Edit Info": "Edit", "dataset.Export": "Export", "dataset.Export Dataset Limit Error": "Failed to export data.", "dataset.Folder Name": "Folder name", @@ -780,7 +780,7 @@ "dataset.Manual collection Tip": "A manual dataset allows you to create an empty container to add data.", "dataset.Move Failed": "Move Error", "dataset.Select Dataset": "Select This Dataset", - "dataset.Select Dataset Tips": "You can only select knowledge bases that use the same index model.", + "dataset.Select Dataset Tips": "You can select knowledge bases only from those using the same index model.", "dataset.Select Folder": "Open folder", "dataset.Training Name": "Data training", "dataset.collections.Collection Embedding": "{{total}} data groups are being indexed.", @@ -791,35 +791,35 @@ "dataset.data.Can not edit": "You do not have permission to edit.", "dataset.data.Default Index": "Default index", "dataset.data.Delete Tip": "Are you sure you want to delete the entry?", - "dataset.data.Index Placeholder": "Text for indexing", + "dataset.data.Index Placeholder": "Enter text for indexing", "dataset.data.Input Success Tip": "Data imported successfully.", "dataset.data.Update Success Tip": "Data updated successfully.", - "dataset.data.edit.Index": "Data index {{amount}}", + "dataset.data.edit.Index": "Data indexes {{amount}}", "dataset.data.edit.divide_content": "Chunk", "dataset.data.input is empty": "This field is required.", "dataset.dataset_name": "Knowledge base name", - "dataset.deleteFolderTips": "Are you sure you want to delete the folder and the knowledge bases it contains? The data cannot be recovered.", + "dataset.deleteFolderTips": "Are you sure you want to delete the folder and knowledge bases in it? Deleted data cannot be recovered.", "dataset.test.noResult": "No match found.", "dataset_data_input_a": "Answer", "dataset_data_input_chunk": "Regular mode", "dataset_data_input_chunk_content": "Content", "dataset_data_input_q": "Question", "dataset_data_input_qa": "Q&A mode", - "dataset_text_model_tip": "Used for preprocessing knowledge base text, such as automatic index generation and Q&A extraction.", + "dataset_text_model_tip": "Used for preprocessing knowledge base text, such as automatic supplemental index generation and Q&A pair extraction.", "date_12_months": "12 months", "date_1_month": "1 month", "date_3_months": "3 months", "date_6_months": "6 months", "deep_rag_search": "Deep search", "delete_api": "Are you sure you want to delete the API key? The key will become invalid immediately, but the corresponding chat logs will not be deleted. Would you like to proceed?", - "delete_failed": "Operation failed.", + "delete_failed": "Deletion failed.", "delete_folder": "Delete folder", "delete_success": "Deleted successfully.", - "delete_warning": "Deletion alert", + "delete_warning": "Confirm", "embedding_model_not_config": "No index model available.", "enable_auth": "Enable authentication", "error.Create failed": "Creation failed", - "error.code_error": "The code is incorrect.", + "error.code_error": "The verification code is incorrect.", "error.fileNotFound": "File not found.", "error.inheritPermissionError": "Permission inheritance error.", "error.invalid_params": "Invalid parameter.", @@ -829,21 +829,21 @@ "error.unKnow": "Error occurred.", "error.upload_file_error_filename": "Failed to upload {{name}}.", "error.upload_image_error": "Upload failed.", - "error.username_empty": "This field is required.", + "error.username_empty": "The Account field is required.", "error_collection_not_exist": "The collection does not exist.", "error_embedding_not_config": "No index model configured.", "error_invalid_resource": "Invalid resource.", - "error_llm_not_config": "No file understanding model configured.", + "error_llm_not_config": "No LLMs available.", "error_un_permission": "No permission.", - "error_vlm_not_config": "No image understanding model configured.", + "error_vlm_not_config": "No VLMs available.", "exit_directly": "Exit", "expired_time": "Expiration time", - "export_to_json": "Export as JSON file", + "export_to_json": "Export as JSON", "extraction_results": "Extraction result", - "failed": "Operation failed.", + "failed": "Failed", "field_name": "Field name", - "folder.empty": "No items selectable in the directory.", - "folder.open_dataset": "Open knowledge base", + "folder.empty": "No items available.", + "folder.open_dataset": "Click to open it.", "folder_description": "Folder description", "free": "Free", "get_QR_failed": "Failed to obtain the QR code.", @@ -854,22 +854,22 @@ "have_done": "Completed", "import_failed": "Import failed.", "import_success": "Import successful.", - "info.buy_extra": "Purchase extra plan", + "info.buy_extra": "Purchase additional plan", "info.csv_download": "Download bulk test template", "info.csv_message": "Read the first column in the CSV file for bulk testing (up to 100 entries per run).", - "info.felid_message": "Field key must contain only letters or digits, and cannot start with a digit.", + "info.felid_message": "The key field must contain only letters or digits, and cannot start with a digit.", "info.free_plan": "If a team using the free edition is inactive for 30 days, its knowledge bases will be cleared automatically.", - "info.include": "Includes the standard plan and extra resource packages.", - "info.node_info": "Adjusting this module will affect the time when tools are called.\nProvide a clear description of the module's function to guide the model in calling the right tools.", - "info.old_version_attention": "The current module version (Advanced orchestration) is outdated. The system will automatically upgrade it to the new version (Workflow).\n\nSome workflows cannot be laid out correctly due to version differences. Please manually connect to the workflows. If the problem persists, try deleting the affected node and add it again.\n\nClick Debug to test the workflow. After debugging is complete, click Save and publish. The new workflow is not saved or active until you click Save and publish.\n\nAuto save will not take effect until you publish the new workflow.", - "info.open_api_notice": "Enter your OpenAI or OneAPI key. The key will be used for AI chats, question classification, and content extraction without extra charges. Make sure the key can be used to access the required models. You can choose FastAI as the GPT model.", - "info.open_api_placeholder": "Request address. Default: official OpenAI address. You can enter a proxy address. Note that \"/v1\" will not be automatically appended to the address.", + "info.include": "Includes the standard plan and additional resource packages.", + "info.node_info": "Changes to this module will affect tool calls.\nProvide a clear description of the module's feature to help the model use the correct tools.", + "info.old_version_attention": "The current module version (Advanced orchestration) is outdated. The system will automatically upgrade it to the new version (Workflow).\n\nSome workflows cannot be arranged correctly due to version differences. Please manually connect to the workflows. If the problem persists, try deleting the affected node and add it again.\n\nClick Debug to test the workflow. After debugging is complete, click Save and publish. The new workflow is not saved or active until you click Save and publish.\n\nAuto saving will not take effect until you publish the new workflow.", + "info.open_api_notice": "Enter your OpenAI or OneAPI key. The key will be used for AI chats, question classifier, and content extraction without consuming credits. Make sure the key has required permissions to access relevant models. You can choose FastAI as the GPT model.", + "info.open_api_placeholder": "Request address. The official OpenAI address will be used by default. You can enter a proxy address. Note that \"/v1\" will not be automatically appended to the address.", "info.resource": "Resource usage", "input.Repeat Value": "Duplicate values exist.", "input_name": "Name", "invalid_time": "Validity", "invalid_variable": "Invalid variable.", - "is_open": "Enable", + "is_open": "Status", "is_requesting": "Sending request...", "is_using": "In use", "item_description": "Field description", @@ -885,12 +885,12 @@ "llm_model_not_config": "No language model available.", "load_failed": "Loading failed.", "logout": "Log out", - "max_quote_tokens": "Max references", + "max_quote_tokens": "Max tokens", "max_quote_tokens_tips": "The maximum number of tokens consumed per search (about 1.7 tokens for a Chinese character and 1 token for an English character)", "mcp_server": "MCP services", "member": "Member", "min_similarity": "Min relevance", - "min_similarity_tip": "Relevance varies across index models. Please run a search test to find the right value. When Result reranking is enabled, results are filtered based on the reranked output.", + "min_similarity_tip": "Relevance varies across index models. Please specify an appropriate value through search testing. When the result reranking feature is enabled, results are filtered based on the reranked output.", "model.billing": "Model billing", "model.model_type": "Model type", "model.name": "Model name", @@ -898,7 +898,7 @@ "model.search_name_placeholder": "Model name", "model.type.chat": "Language model", "model.type.embedding": "Index model", - "model.type.reRank": "Reranker model", + "model.type.reRank": "Rerank model", "model.type.stt": "Speech recognition", "model.type.tts": "Text-to-speech", "model_alicloud": "Alibaba Cloud", @@ -908,77 +908,77 @@ "model_doubao": "Doubao", "model_ernie": "ERNIE Bot", "model_hunyuan": "Tencent Hunyuan", - "model_intern": "InternLM", + "model_intern": "Intern AI", "model_moka": "Moka AI", "model_moonshot": "Moonshot AI", "model_other": "Other", "model_ppio": "PPIO", - "model_qwen": "Alibaba Qwen", + "model_qwen": "Qwen", "model_siliconflow": "SiliconFlow", - "model_sparkdesk": "iFlytek Spark", + "model_sparkdesk": "iFLYTEK Spark", "model_stepfun": "StepFun", "model_yi": "01.AI", "month": "Monthly", "move.confirm": "Confirm", "move_success": "Moved successfully.", - "move_to": "Move to", + "move_to": "Move", "name": "Name", "name_is_empty": "Name is required.", "navbar.Account": "Account", "navbar.Chat": "Chat", - "navbar.Datasets": "Knowledge base", + "navbar.Datasets": "Knowledge", "navbar.Studio": "Studio", "navbar.Toolkit": "Toolbox", "navbar.Tools": "Tool", - "new_create": "Create", + "new_create": "Add", "next_step": "Next", "no": "No", "no_child_folder": "No subdirectory available.", - "no_intro": "No description available.", + "no_intro": "", "no_laf_env": "LAF environment not configured.", - "no_more_data": "No more content.", - "no_pay_way": "No available payment channel.", - "no_select_data": "No available value.", + "no_more_data": "No data available.", + "no_pay_way": "No payment channel available.", + "no_select_data": "No value available.", "not_model_config": "No model configured.", "not_open": "Not enabled", - "not_permission": "Team activity logs are not available in your current plan.", + "not_permission": "Member operation logs are not available in your current plan.", "not_support": "Not supported", "not_support_wechat_image": "WeChat image", - "not_yet_introduced": "No description available.", - "open_folder": "Open folder", + "not_yet_introduced": "", + "open_folder": "Click to open the folder.", "option": "Option", "page": "Page", - "page_center": "Center page", + "page_center": "Centered", "pay.amount": "Amount", "pay.error_desc": "Error occurred while switching payment method.", "pay.noclose": "After the payment is complete, wait for the system to update automatically.", - "pay.package_tip.buy": "This plan will take effect after your current plan expires because its level is lower than that of your current plan.\nYou can check plan usage in Account > Profile > Plan details.", + "pay.package_tip.buy": "This plan will take effect after your current plan expires because it is of a lower tier than your current plan.\nYou can check plan usage in Account > Profile > Plan details.", "pay.package_tip.renewal": "Renewing... You can check plan usage in Account > Profile > Plan details.", - "pay.package_tip.upgrade": "This plan will take effect immediately, and your current plan will be applied later because the level of this plan is higher than that of your current plan. You can check plan usage in Account > Profile > Plan details.", - "pay.wechat": "Please scan the QR code via WeChat to pay: {{price}} CNY\nDo not close this page before payment is complete.", - "pay.wx_payment": "WeChat", + "pay.package_tip.upgrade": "This plan will take effect immediately, and your current plan will take effect later because it is of a higher tier than your current plan. You can check plan usage in Account > Profile > Plan details.", + "pay.wechat": "Use WeChat to scan the QR code and pay {{price}} CNY\nDo not close this page before the payment is complete.", + "pay.wx_payment": "WeChat Pay", "pay.yuan": "{{amount}} CNY", "pay_alipay_payment": "Alipay", - "pay_corporate_payment": "Corporate account", + "pay_corporate_payment": "Corporate payment", "pay_money": "Amount", "pay_success": "Payment successful.", - "pay_year_tip": "Get 2 extra months free when you pay for 10.", + "pay_year_tip": "Pay for 10 months, enjoy 1 year!", "permission.Collaborator": "Collaborators", "permission.Default permission": "Default permission", "permission.Manage": "Manage", "permission.No InheritPermission": "Permissions are restricted and no longer inherited from the parent folder.", "permission.Not collaborator": "No collaborators available.", - "permission.Owner": "Creator", + "permission.Owner": "Created By", "permission.Permission": "Permission", "permission.Permission config": "Permissions", "permission.Private": "Private", - "permission.Private Tip": "Only available to myself", + "permission.Private Tip": "Only me", "permission.Public": "Collaboration", "permission.Public Tip": "Available to all team members", "permission.Remove InheritPermission Confirm": "This operation will disable permission inheritance. Would you like to proceed?", - "permission.Resume InheritPermission Confirm": "Do you want to enable permission inheritance again?", - "permission.Resume InheritPermission Failed": "Operation failed.", - "permission.Resume InheritPermission Success": "Operation successful.", + "permission.Resume InheritPermission Confirm": "Are you sure you want to restore permissions to be inherited from the parent folder?", + "permission.Resume InheritPermission Failed": "Restoration failed.", + "permission.Resume InheritPermission Success": "Restored successfully.", "permission.change_owner": "Transfer ownership", "permission.change_owner_failed": "Failed to transfer the ownership.", "permission.change_owner_placeholder": "Username", @@ -986,18 +986,18 @@ "permission.change_owner_tip": "You will lose related permissions after the ownership transfer.", "permission.change_owner_to": "Transfer to", "permission.manager": "Admin", - "permission.read": "Read permissions", - "permission.write": "Write permissions", - "permission_other": "Other permissions (one or more options)", + "permission.read": "Read permission", + "permission.write": "Write permission", + "permission_other": "Other (multi-select supported)", "please_input_name": "Name is required.", "plugin.App": "Select app", "plugin.Currentapp": "Current app", "plugin.Description": "Description", "plugin.Edit Http Plugin": "Edit HTTP plugin", - "plugin.Enter PAT": "Enter Personal Access Token (PAT)", + "plugin.Enter PAT": "Personal access token", "plugin.Get Plugin Module Detail Failed": "Error occurred while obtaining plugin information.", "plugin.Import Plugin": "Import HTTP plugin", - "plugin.Import from URL": "Import from URL https://xxxx", + "plugin.Import from URL": "[Import from URL] https://xxxx", "plugin.Intro": "Plugin description", "plugin.Invalid Env": "LAF environment error.", "plugin.Invalid Schema": "Invalid schema.", @@ -1008,36 +1008,36 @@ "plugin.Plugin List": "Plugins", "plugin.Search_app": "App name", "plugin.Set Name": "Name", - "plugin.contribute": "Third-party plugins", + "plugin.contribute": "Plugins", "plugin.go to laf": "Create", "plugin.path": "Path", - "price_over_wx_limit": "The payment exceeded the quota. WeChat Pay only supports payments under 6000 CNY.", + "price_over_wx_limit": "The payment exceeded the quota. WeChat Pay only supports transactions of 6000 CNY or less.", "prompt_input_placeholder": "Prompt", "psw_inconsistency": "Passwords do not match.", "question_feedback": "Help", "read_course": "Guide", "read_doc": "View documentation", "read_quote": "View reference", - "redo_tip": "Redo (Ctrl+Shift+Z)", + "redo_tip": "Redo (Ctrl + Shift + Z)", "redo_tip_mac": "Redo (⌘+Shift+Z)", "request_end": "All items loaded.", "request_error": "Request error.", - "request_more": "Load more", + "request_more": "Click to load more.", "required": "Required", "rerank_weight": "Weight", "resume_failed": "Restoration failed.", - "root_folder": "Root directory", - "save_failed": "Error occurred during the operation.", + "root_folder": "Root", + "save_failed": "Failed to save the settings.", "save_success": "Saved successfully.", "scan_code": "Scan to pay", "search_tool": "Tool name", "secret_key": "Secret key", - "secret_tips": "Saved values will not be returned in plain text again.", - "select_count_num": "{{num}} item selected", + "secret_tips": "The value will not be shown in plain text after it is saved.", + "select_count_num": "{{num}} item selected", "select_file_failed": "File Selection Failed", - "select_reference_variable": "Variable", + "select_reference_variable": "Select variable", "select_template": "Select template", - "set_avatar": "Upload tool image", + "set_avatar": "Click to upload an icon.", "share_link": "Sharing link", "speech_error_tip": "Failed to convert the speech to text.", "speech_not_support": "Your browser does not support the speech-to-text feature.", @@ -1052,38 +1052,38 @@ "support.openapi.Copy success": "API address copied successfully.", "support.openapi.New api key": "New API key", "support.openapi.New api key tip": "Please save your key properly. It will not be displayed again.", - "support.outlink.Delete link tip": "Are you sure you want to delete the login-free link? This link will immediately become invalid, but chat logs will be retained.", - "support.outlink.Max usage points": "Max points", - "support.outlink.Max usage points tip": "The maximum number of points that can be consumed in the link. Enter -1 for no limit.", - "support.outlink.Usage points": "Points consumed", + "support.outlink.Delete link tip": "Are you sure you want to delete this link? This link will immediately become invalid, but the chat history will be preserved.", + "support.outlink.Max usage points": "Max credits", + "support.outlink.Max usage points tip": "The maximum number of credits that can be used with this link. -1 indicates no limit.", + "support.outlink.Usage points": "Credits consumed", "support.outlink.share.Chat_quote_reader": "Full text reader", "support.outlink.share.Full_text tips": "Read the full dataset from which the referenced segment was taken.", "support.outlink.share.Response Quote": "Referenced content", - "support.outlink.share.Response Quote tips": "View referenced content found in knowledge base searches. Full documents and external links are not accessible.", + "support.outlink.share.Response Quote tips": "View referenced content found in knowledge base searches. Full documents and referenced websites are not accessible.", "support.outlink.share.running_node": "Running node", "support.outlink.share.show_complete_quote": "View source", - "support.outlink.share.show_complete_quote_tips": "View and download the full referenced document, or open the source website.", + "support.outlink.share.show_complete_quote_tips": "View and download full referenced documents, or redirect to the referenced websites.", "support.permission.Permission": "Permission", - "support.standard.AI Bonus Points": "AI points", + "support.standard.AI Bonus Points": "Credits", "support.standard.due_date": "Expiration time", - "support.standard.storage": "Max shards", + "support.standard.storage": "Max indexes", "support.standard.type": "Type", "support.team.limit.No permission rerank": "You do not have permission to use the result reranking feature. Please upgrade your plan.", "support.user.Avatar": "Profile image", - "support.user.Go laf env": "Go to {{env}} to obtain a PAT token", + "support.user.Go laf env": "Obtain PAT token from {{env}}", "support.user.Laf account course": "View guide for binding LAF account", "support.user.Laf account intro": "Bind your LAF account to enable the LAF module in workflows for online coding.", "support.user.Need to login": "Please log in first.", - "support.user.Price": "Billing", + "support.user.Price": "Billing standard", "support.user.User self info": "Profile", "support.user.auth.Sending Code": "Sending...", "support.user.auth.get_code": "Send", - "support.user.auth.get_code_again": "seconds", - "support.user.captcha_placeholder": "Please enter the verification code", + "support.user.auth.get_code_again": "s", + "support.user.captcha_placeholder": "", "support.user.info.bind_notification_error": "Operation failed.", - "support.user.info.bind_notification_hint": "To ensure service continuity, please specify an account to receive renewal and other notifications.", + "support.user.info.bind_notification_hint": "For service continuity, please specify an account to receive renewal and other notifications.", "support.user.info.bind_notification_success": "Operation successful.", - "support.user.info.code_required": "Verification code is required.", + "support.user.info.code_required": "Enter the verification code", "support.user.info.notification_receiving_hint": "Notification recipient", "support.user.info.verification_code": "Verification code", "support.user.inform.System message": "System message", @@ -1098,141 +1098,138 @@ "support.user.login.Provider error": "Login error occurred. Please try again later.", "support.user.login.Username": "Username", "support.user.login.Wechat": "WeChat login", - "support.user.login.can_not_login": "Login failed. Contact Customer Service.", + "support.user.login.can_not_login": "Contact support", "support.user.login.error": "Login error occurred.", "support.user.login.security_failed": "Verification failed.", - "support.user.login.wx_qr_login": "Scan QR code via WeChat", - "support.user.login.sxf_com": "Sangfor Commercial Edition", - "support.user.login.fastgpt_sxf_com": "FastGPT Sangfor Commercial Edition", - "support.user.login.sxf_com_platform_desc": "Enable every digital user to more easily develop and continuously optimize their own AI applications, while continuously accumulating data and AI development capabilities. Through AI application-oriented development workflows, we provide various knowledge enhancement tools to improve the data quality of application building, and help users optimize applications in a self-contained loop through application performance evaluation and assisted positioning.", + "support.user.login.wx_qr_login": "Scan with WeChat", "support.user.logout.confirm": "Are you sure you want to log out?", - "support.user.team.Dataset usage": "Knowledge base capacity", + "support.user.team.Dataset usage": "Knowledge base indexes", "support.user.team.Team Tags Async Success": "Sync completed", "support.user.team.member": "Member", - "support.wallet.Ai point every thousand tokens": "{{points}} points/1K tokens", - "support.wallet.Ai point every thousand tokens_input": "Input: {{points}} points/1K tokens", - "support.wallet.Ai point every thousand tokens_output": "Output: {{points}} points/1K tokens", + "support.wallet.Ai point every thousand tokens": "{{points}} credits/1K tokens", + "support.wallet.Ai point every thousand tokens_input": "Input: {{points}} credits/1K tokens", + "support.wallet.Ai point every thousand tokens_output": "Output: {{points}} credits/1K tokens", "support.wallet.Amount": "Amount", - "support.wallet.App_amount_not_sufficient": "The number of apps has reached the maximum. Please upgrade the plan.", + "support.wallet.App_amount_not_sufficient": "The number of apps reaches the maximum. Please upgrade the plan.", "support.wallet.Buy": "Purchase", - "support.wallet.Dataset_amount_not_sufficient": "The number of knowledge bases has reached the maximum. Please upgrade the plan.", - "support.wallet.Dataset_not_sufficient": "The knowledge base capacity is insufficient. Please upgrade the plan or purchase extra capacity.", - "support.wallet.Not sufficient": "Your AI points are insufficient. Please upgrade the plan or purchase more AI points.", + "support.wallet.Dataset_amount_not_sufficient": "The number of knowledge basesreaches the maximum. Please upgrade the plan.", + "support.wallet.Dataset_not_sufficient": "The knowledge base indexes are insufficient. Please upgrade the plan or purchase additional storage.", + "support.wallet.Not sufficient": "Your credits are insufficient. Please upgrade the plan or purchase more credits.", "support.wallet.Plan expired time": "Expiration time", "support.wallet.Standard Plan Detail": "Plan details", - "support.wallet.Team_member_over_size": "The number of team members has reached the maximum. Please upgrade the plan.", + "support.wallet.Team_member_over_size": "The number of team members reaches the maximum. Please upgrade the plan.", "support.wallet.To read plan": "View plan", - "support.wallet.amount_0": "Value cannot be 0.", - "support.wallet.apply_invoice": "Request invoice", + "support.wallet.amount_0": "The value cannot be 0.", + "support.wallet.apply_invoice": "Request fapiao", "support.wallet.bill.Number": "Order ID", "support.wallet.bill.Status": "Status", "support.wallet.bill.Type": "Order type", "support.wallet.bill.payWay.Way": "Payment method", "support.wallet.bill.payWay.alipay": "Alipay", "support.wallet.bill.payWay.balance": "Balance", - "support.wallet.bill.payWay.bank": "Corporate account", - "support.wallet.bill.payWay.wx": "WeChat", - "support.wallet.bill.status.closed": "Disabled", + "support.wallet.bill.payWay.bank": "Corporate payment", + "support.wallet.bill.payWay.wx": "WeChat Pay", + "support.wallet.bill.status.closed": "Closed", "support.wallet.bill.status.notpay": "Unpaid", "support.wallet.bill.status.refund": "Refunded", "support.wallet.bill.status.success": "Payment successful.", "support.wallet.bill_detail": "Bill details", "support.wallet.bill_tag.bill": "Billing records", - "support.wallet.bill_tag.default_header": "Default header", - "support.wallet.bill_tag.invoice": "Invoicing records", - "support.wallet.billable_invoice": "Invoiceable bill", - "support.wallet.buy_ai_points": "Purchase AI points", + "support.wallet.bill_tag.default_header": "Default title", + "support.wallet.bill_tag.invoice": "Fapiao issuing history", + "support.wallet.billable_invoice": "Bill available for fapiao issuing", + "support.wallet.buy_ai_points": "Purchase credits", "support.wallet.buy_dataset_capacity": "Purchase knowledge base indexes", "support.wallet.buy_resource": "Purchase resource packages", - "support.wallet.has_invoice": "Invoice issued", - "support.wallet.invoice_amount": "Invoice amount", + "support.wallet.has_invoice": "Fapiao issued", + "support.wallet.invoice_amount": "Fapiao amount", "support.wallet.invoice_data.bank": "Bank name", "support.wallet.invoice_data.bank_account": "Account number", "support.wallet.invoice_data.company_address": "Company address", "support.wallet.invoice_data.company_phone": "Company phone", "support.wallet.invoice_data.email": "Email address", - "support.wallet.invoice_data.need_special_invoice": "VAT invoice required", + "support.wallet.invoice_data.need_special_invoice": "Special VAT fapiao", "support.wallet.invoice_data.organization_name": "Organization", "support.wallet.invoice_data.unit_code": "Unified social credit code", - "support.wallet.invoice_detail": "Invoice details", - "support.wallet.invoice_info": "The invoice will be sent to the specified email address within 3-7 workdays. Please wait.", - "support.wallet.invoicing": "Issue invoice", + "support.wallet.invoice_detail": "Fapiao details", + "support.wallet.invoice_info": "The fapiao will be emailed to the specified address within 3-7 business days.", + "support.wallet.invoicing": "Issue fapiao", "support.wallet.moduleName.qa": "Q&A generation", "support.wallet.noBill": "No data available.", "support.wallet.no_invoice": "No data available.", - "support.wallet.subscription.AI points": "AI points", - "support.wallet.subscription.AI points click to read tip": "Each AI model call consumes AI points (similar to tokens). Click to view detailed billing rules.", - "support.wallet.subscription.AI points usage": "AI point usage", - "support.wallet.subscription.AI points usage tip": "Each AI model call consumes AI points. For details, refer to the billing standard above.", - "support.wallet.subscription.Ai points": "AI point billing standard", + "support.wallet.subscription.AI points": "Credits", + "support.wallet.subscription.AI points click to read tip": "Model calls consume credits (similar to tokens). Click to view detailed billing rules.", + "support.wallet.subscription.AI points usage": "Credit usage", + "support.wallet.subscription.AI points usage tip": "Model calls consume credits. For details, refer to the billing standard above.", + "support.wallet.subscription.Ai points": "Credit billing standard", "support.wallet.subscription.Current plan": "Current plan", "support.wallet.subscription.Dataset size": "Knowledge base indexes", - "support.wallet.subscription.Extra ai points": "Extra AI points", - "support.wallet.subscription.Extra ai points description": "The more AI points you purchase, the longer validity period you get.", + "support.wallet.subscription.Extra ai points": "Additional credits", + "support.wallet.subscription.Extra ai points description": "The more credits you purchase, the longer validity period you get.", "support.wallet.subscription.Extra dataset description": "Extend the validity of knowledge base indexes as needed.", - "support.wallet.subscription.Extra dataset size": "Extra knowledge base capacity", + "support.wallet.subscription.Extra dataset size": "Additional knowledge base indexes", "support.wallet.subscription.Extra dataset unit": "groups/month", - "support.wallet.subscription.Extra plan": "Extra packages", - "support.wallet.subscription.Extra plan tip": "Purchase extra packages if the standard plan is not enough.", - "support.wallet.subscription.FAQ": "FAQ", + "support.wallet.subscription.Extra plan": "Additional resource packages", + "support.wallet.subscription.Extra plan tip": "Purchase additional resource packages to continue using the plan.", + "support.wallet.subscription.FAQ": "FAQs", "support.wallet.subscription.Month amount": "Months", "support.wallet.subscription.Next plan": "Upcoming plans", - "support.wallet.subscription.Points amount": "Points", + "support.wallet.subscription.Points amount": "Credits", "support.wallet.subscription.Stand plan level": "Subscription plan", "support.wallet.subscription.Sub plan": "Subscription plan", - "support.wallet.subscription.Sub plan tip": "Use {{title}} for free or upgrade to a higher plan.", + "support.wallet.subscription.Sub plan tip": "Use {{title}} for free or upgrade to a higher-level plan.", "support.wallet.subscription.Team plan and usage": "Plans and usage", "support.wallet.subscription.Training weight": "Training priority: {{weight}}", - "support.wallet.subscription.Update extra ai points": "Extra AI points", - "support.wallet.subscription.Update extra ai points tips": "Points are valid upon purchase and invalid upon expiration.", - "support.wallet.subscription.Update extra dataset size": "Extra storage", - "support.wallet.subscription.Update extra dataset tips": "When knowledge base indexes expire, existing data is retained but cannot be added or modified.", - "support.wallet.subscription.Update extra price": "Unit price", + "support.wallet.subscription.Update extra ai points": "Additional credits", + "support.wallet.subscription.Update extra ai points tips": "Purchased credits are added to an account instantly but can no longer be used after the expiration date.", + "support.wallet.subscription.Update extra dataset size": "Additional indexes", + "support.wallet.subscription.Update extra dataset tips": "When knowledge base indexes expire, existing data is retained, but changes or additions to the data are not allowed.", + "support.wallet.subscription.Update extra price": "Price", "support.wallet.subscription.Upgrade plan": "Upgrade plan", - "support.wallet.subscription.ai_model": "AI language model", - "support.wallet.subscription.function.History store": "Max chat retention: {{amount}} days", - "support.wallet.subscription.function.Max app": "Max apps: {{amount}}", - "support.wallet.subscription.function.Max dataset": "Max knowledge bases: {{amount}}", - "support.wallet.subscription.function.Max dataset size": "Knowledge base indexes: {{amount}} groups", - "support.wallet.subscription.function.Max members": "Max team members: {{amount}}", - "support.wallet.subscription.function.Points": "AI points: {{amount}}", + "support.wallet.subscription.ai_model": "Language model", + "support.wallet.subscription.function.History store": "{{amount}} days chat history", + "support.wallet.subscription.function.Max app": "{{amount}} apps", + "support.wallet.subscription.function.Max dataset": "{{amount}} knowledge bases", + "support.wallet.subscription.function.Max dataset size": "{{amount}} knowledge base index groups", + "support.wallet.subscription.function.Max members": "{{amount}} team members", + "support.wallet.subscription.function.Points": "{{amount}} credits", "support.wallet.subscription.mode.Month": "Monthly", "support.wallet.subscription.mode.Period": "Periodic", "support.wallet.subscription.mode.Year": "Yearly", "support.wallet.subscription.mode.Year sale": "2 months free", - "support.wallet.subscription.point": "Points", + "support.wallet.subscription.point": "Credits", "support.wallet.subscription.standardSubLevel.custom": "Custom edition", "support.wallet.subscription.standardSubLevel.enterprise": "Enterprise edition", - "support.wallet.subscription.standardSubLevel.enterprise_desc": "Suitable for SMEs to build knowledge base apps in the production environment.", + "support.wallet.subscription.standardSubLevel.enterprise_desc": "For medium-sized teams", "support.wallet.subscription.standardSubLevel.experience": "Trial edition", - "support.wallet.subscription.standardSubLevel.experience_desc": "Unlocks full FastGPT features.", + "support.wallet.subscription.standardSubLevel.experience_desc": "Unlimited access to all features", "support.wallet.subscription.standardSubLevel.free": "Free edition", - "support.wallet.subscription.standardSubLevel.free desc": "Core features available for free trial. Knowledge bases will be cleared for users who are inactive for 30 days.", + "support.wallet.subscription.standardSubLevel.free desc": "Free trial of core features Knowledge bases will be cleared after 30 days of inactivity.", "support.wallet.subscription.standardSubLevel.team": "Team edition", - "support.wallet.subscription.standardSubLevel.team_desc": "Suitable for small teams to build knowledge base apps and provide external services.", + "support.wallet.subscription.standardSubLevel.team_desc": "For small teams", "support.wallet.subscription.status.active": "Active", "support.wallet.subscription.status.expired": "Expired", "support.wallet.subscription.status.inactive": "Pending", - "support.wallet.subscription.team_operation_log": "Log team operations", - "support.wallet.subscription.token_compute": "View online token calculator", + "support.wallet.subscription.team_operation_log": "Log member operations", + "support.wallet.subscription.token_compute": "Click to view online token calculator", "support.wallet.subscription.type.balance": "Balance top-up", "support.wallet.subscription.type.extraDatasetSize": "Knowledge base expansion", - "support.wallet.subscription.type.extraPoints": "AI point package", + "support.wallet.subscription.type.extraPoints": "Credit plan", "support.wallet.subscription.type.standard": "Plan subscription", "support.wallet.subscription.web_site_sync": "Website sync", - "support.wallet.usage.Ai model": "AI model", + "support.wallet.usage.Ai model": "Model", "support.wallet.usage.App name": "App name", - "support.wallet.usage.Audio Speech": "Speech playback", + "support.wallet.usage.Audio Speech": "Text-to-speech", "support.wallet.usage.Bill Module": "Billing module", "support.wallet.usage.Duration": "Duration (s)", "support.wallet.usage.Module name": "Module name", - "support.wallet.usage.Optimize Prompt": "Prompt word optimization", + "support.wallet.usage.Optimize Prompt": "Prompt word optimization", "support.wallet.usage.Source": "Source", "support.wallet.usage.Text Length": "Text length", "support.wallet.usage.Time": "Time generated", "support.wallet.usage.Token Length": "Token length", "support.wallet.usage.Total": "Total amount", - "support.wallet.usage.Total points": "AI points consumed", + "support.wallet.usage.Total points": "Credits consumed", "support.wallet.usage.Usage Detail": "Usage details", "support.wallet.usage.Whisper": "Speech-to-text", "sync_link": "Sync link", @@ -1246,17 +1243,17 @@ "templateTags.Image_generation": "Image generation", "templateTags.Office_services": "Office service", "templateTags.Roleplay": "Role playing", - "templateTags.Web_search": "Online search", + "templateTags.Web_search": "Search", "templateTags.Writing": "Content creation", "template_market": "Templates", - "textarea_variable_picker_tip": "Enter a slash (/) to select a variable", + "textarea_variable_picker_tip": "Enter a slash (/) to select a variable.", "to_dataset": "Go to knowledge base", "total_num": "Total: {{num}}", "ui.textarea.Magnifying": "Zoom in", "un_used": "Unused", "unauth_token": "Your credential has expired. Please log in again.", - "undo_tip": "Undo (Ctrl+Z)", - "undo_tip_mac": "Undo (⌘+Z)", + "undo_tip": "Undo (Ctrl + Z)", + "undo_tip_mac": "Undo (⌘ + Z)", "unit.character": "characters", "unit.minute": "Minute", "unit.seconds": "s", @@ -1275,23 +1272,23 @@ "user.Avatar": "Profile image", "user.Change": "Change", "user.Copy invite url": "Copy invitation link", - "user.Edit name": "Change nickname", + "user.Edit name": "Click to change the name.", "user.Invite Url": "Invitation link", - "user.Invite url tip": "Friends who register through this link will be permanently linked to your account. Each time they top up, you will receive a balance reward.\nIf they register with a mobile number, you will obtain an immediate 5 CNY bonus.\nAll rewards are credited to your default team account.", + "user.Invite url tip": "Friends who register through this link will be permanently linked to your account. Each time they top up, you will receive a balance reward.\nIf they register with a mobile number, you will get an immediate 5 CNY bonus.\nAll rewards are credited to your default team account.", "user.Laf Account Setting": "LAF account", "user.Language": "Language", "user.Member Name": "Nickname", "user.No_right_to_reset_password": "You do not have permission to reset the password.", "user.Notification Receive": "Notification recipient", "user.Notification Receive Bind": "Please specify a notification recipient first.", - "user.Old password is error": "The old password is incorrect", + "user.Old password is error": "The current password is incorrect", "user.OpenAI Account Setting": "OpenAI account", "user.Password": "Password", "user.Password has no change": "The new and current passwords cannot be the same.", "user.Pay": "Top up", "user.Promotion": "Promotion", "user.Promotion Rate": "Cashback rate", - "user.Promotion rate tip": "You will receive a balance reward proportionate to your friend's top-up amount.", + "user.Promotion rate tip": "You will receive a balance reward based on your friend's top-up amount.", "user.Replace": "Replace", "user.Set OpenAI Account Failed": "Error occurred while configuring the OpenAI account.", "user.Team": "Team", @@ -1309,8 +1306,8 @@ "user.no_usage_records": "No data available.", "user.old_password": "Current password", "user.password_message": "Password must contain 4-60 characters.", - "user.password_tip": "Password must be at least 8 characters long and contain at least 2 of the following: digits, letters, and special characters.", - "user.reset_password": "Reset", + "user.password_tip": "Must be at least 8 characters long and contain at least 2 of the following: digits, letters, and special characters.", + "user.reset_password": "Reset password", "user.reset_password_tip": "Initial password not set or password unchanged for a long time. Please reset your password.", "user.team.Balance": "Team balance", "user.team.Check Team": "Switch", @@ -1331,12 +1328,12 @@ "user.team.invite.Accepted": "Joined", "user.team.invite.Deal Width Footer Tip": "It will automatically close after processing", "user.team.invite.Reject": "The invitation was rejected.", - "user.team.member.Confirm Leave": "Are you sure you want to leave the team?\nAll your resources in this team (apps, knowledge bases, folders, and groups you manage) will be transferred to the team owner.", + "user.team.member.Confirm Leave": "Are you sure you want to leave the team?\nAll your resources in this team (apps, knowledge bases, folders, and managed groups) will be transferred to the team owner.", "user.team.member.active": "Joined", "user.team.member.reject": "Reject", "user.team.member.waiting": "Pending", "user.team.role.Admin": "Admin", - "user.team.role.Owner": "Creator", + "user.team.role.Owner": "Created By", "user.team.role.Visitor": "Guest", "user.team.role.writer": "Members with write permissions", "user.type": "Type", @@ -1351,6 +1348,22 @@ "zoomin_tip_mac": "Zoom out (⌘ -)", "zoomout_tip": "Zoom in (Ctrl +)", "zoomout_tip_mac": "Zoom in (⌘ +)", - "no_database_connection": "还没有连接数据库", - "click_config_database": "点击配置数据库" -} + "no_database_connection": "No database connected.,", + "click_config_database": "Click to configure database.", + "annotation_answer": "Marked answers", + "core.dataset.search.Database search": "Database search", + "search_model": "Search model", + "search_model_desc": "Generates SQL statements for database search, performs search and aggregation, and produces chat text.", + "search_model_tip": "Larger non-reasoning models generally generate better results.", + "other_knowledge_base": "Other knowledge bases", + "database_search": "Database search", + "table_not_exist": "Not exist", + "core.app.workflow.search_knowledge.database": "数据库", + "core.chat.logs.evaluation": "评估测试", + "support.wallet.subscription.eval_items_count": "单次评测数据条数: {{count}} 条", + "core.dataset.table": "数据表", + "core.dataset.search.mode.database": "数据库知识库检索", + "core.dataset.search.mode.database desc": "使用向量检索查找数据库中可能相关的表和列", + "core.dataset.training.databaseSchema mode": "数据库结构", + "core.dataset.import.databaseSchema Tip": "对数据库中的表信息进行自动处理,使其更利于检索,以提高SQL生成的准确率" +} \ No newline at end of file diff --git a/packages/web/i18n/en/dashboard_evaluation.json b/packages/web/i18n/en/dashboard_evaluation.json index 926534e93ac2..0899a31fae6d 100644 --- a/packages/web/i18n/en/dashboard_evaluation.json +++ b/packages/web/i18n/en/dashboard_evaluation.json @@ -1,16 +1,16 @@ { "Action": "Operation", "Evaluation_app": "App", - "Evaluation_app_tip": "You can select a simple app or a workflow without user interaction nodes. Plugins are not supported.", - "Evaluation_file": "Evaluation file", - "Evaluation_model": "Evaluation model", + "Evaluation_app_tip": "Supports a simple app or a workflow without user interaction nodes. Plugins are not supported.", + "Evaluation_file": "File", + "Evaluation_model": "Model", "Executor": "Operator", "Overall_score": "Score", "Progress": "Progress", "Start_end_time": "Start/End Time", - "Task_name_placeholder": "Please enter a task name", + "Task_name_placeholder": "", "answer": "Standard answer", - "app_required": "Select", + "app_required": "Please select an app first.", "app_response": "App output", "back": "Exit", "check_error": "Verification failed.", @@ -23,12 +23,12 @@ "data": "Data", "data_list": "Details", "detail": "Details", - "error": "Abnormal", - "error_tooltip": "Abnormal task detected.\nClick to view task details.", - "eval_file_check_error": "Failed to verify the evaluation file.", + "error": "Error", + "error_tooltip": "A task encountered error.\nClick to view task details.", + "eval_file_check_error": "Evaluation file verification failed.", "evaluating": "Evaluating", "evaluation": "App evaluation", - "evaluation_export_title": "Question, Standard answer, Actual answer, Status, Score", + "evaluation_export_title": "Question,Standard answer,Actual answer,Status,Score", "evaluation_file_max_size": "Entries: {{count}}", "export": "Export", "file_required": "Select", @@ -37,258 +37,339 @@ "paused": "Paused", "question": "Question", "queuing": "Waiting", - "search_task": "Search task", + "search_task": "Task", "standard_response": "Standard output", "start_evaluation": "Evaluate", "stauts": "Status", "task_creating": "Creating task", "task_detail": "Task details", "task_name": "Task name", - "team_has_running_evaluation": "The team already has an app evaluation task in progress. Please wait until it is complete.", - "template_csv_file_select_tip": "Must be a {{fileType}} file strictly consistent with the template.", + "team_has_running_evaluation": "An app evaluation task is in progress. Please wait until it is complete.", + "template_csv_file_select_tip": "Must be a {{fileType}} file that strictly follows the template.", "variables": "Global variables", - "all_apps": "全部应用", - "search_evaluation_task": "搜索任务名或应用版本", - "create_new_task": "新建任务", - "task_name_column": "任务名", - "progress_column": "进度", - "evaluation_app_column": "评测应用", - "app_version_column": "应用版本", - "evaluation_result_column": "评测结果", - "start_finish_time_column": "开始时间/完成时间", - "executor_column": "执行人", - "waiting": "等待中", - "evaluating_status": "评测中", - "completed_status": "已完成", - "queuing_status": "排队中", - "running_status": "进行中", - "error_data_tooltip": "{{count}} 条数据执行异常,可点击查看详情", - "rename": "重命名", - "delete": "删除", - "confirm_delete_task": "确认删除该任务?", - "evaluation_tasks_tab": "评测任务", - "evaluation_tasks": "评测任务", - "evaluation_datasets_tab": "评测数据集", - "evaluation_dimensions_tab": "评测维度", - "create_new": "新建", - "retry_error_data": "重试异常数据", - "dataset_name_placeholder": "名称", - "create_new_dataset": "新建", - "smart_generation": "智能生成", - "file_import": "文件导入", - "confirm_delete_dataset": "确认删除该数据集吗?", - "error_details": "异常详情", - "status_queuing": "排队中", - "status_parsing": "文件解析中", - "status_generating": "数据生成中", - "status_generate_error": "生成异常", - "status_ready": "已就绪", - "status_parse_error": "解析异常", - "click_to_view_details": "点击查看详情", - "table_header_name": "名称", - "table_header_data_count": "数据量", - "table_header_time": "创建/更新时间", - "table_header_status": "状态", - "table_header_creator": "创建人", - "create_dimension": "新建", - "search_dimension": "搜索评测维度", - "delete_failed": "删除失败", - "delete_success": "删除成功", - "builtin": "内置", - "confirm_delete_dimension": "确认删除该维度?", - "dimension_name": "维度名", - "description": "介绍", - "create_update_time": "创建/更新时间", - "creator": "创建人", - "all": "全部", - "app": "应用", - "citation_template": "引用模板", - "correctness": "正确性", - "conciseness": "简洁性", - "harmfulness": "有害性", - "controversiality": "争议性", - "creativity": "创造性", - "criminality": "犯罪性", - "depth": "深度性", - "details": "细节性", - "dimension_name_label": "维度名", - "dimension_description_label": "维度描述", - "prompt_label": "提示词", - "citation_template_button": "引用模板", - "test_run_title": "试运行", - "question_label": "问题", - "question_placeholder": "请输入问题内容", - "answer_label": "答案", - "reference_answer_label": "参考答案", - "reference_answer_placeholder": "请输入参考答案", - "actual_answer_label": "实际回答", - "actual_answer_placeholder": "请输入实际回答", - "run_result_label": "运行结果", - "start_run_button": "开始运行", - "running_text": "运行中", - "run_success": "运行成功", - "run_failed": "运行失败", - "not_run": "未运行", - "score_unit": "分", - "error_info_label": "报错信息:", - "no_feedback_text": "暂无反馈内容", - "dimension_create_back": "退出", - "dimension_create_test_run": "试运行", - "dimension_create_confirm": "确认", - "dimension_create_success": "创建成功", - "dimension_create_name_required": "请输入名称", - "dimension_create_prompt_required": "请输入提示词", - "dimension_get_data_failed": "获取维度数据失败", - "dimension_data_not_exist": "维度数据不存在", - "dimension_update_success": "更新成功", - "dimension_update_failed": "更新失败", - "dimension_name_required": "请输入名称", - "dimension_back": "退出", - "dimension_test_run": "试运行", - "dimension_save": "保存", - "file_import_back": "退出", - "file_import_name_label": "数据集名", - "file_import_name_placeholder": "请输入名称", - "file_import_select_file": "请选择文件", - "file_import_success": "文件导入成功", - "file_import_file_label": "文件", - "file_import_download_template": "点击下载文件模板", - "file_import_download_template_tip": "下载模板文件以了解正确的文件格式", - "file_import_auto_evaluation_label": "导入后自动进行数据质量评测", - "file_import_auto_evaluation_tip": "开启后将自动对导入的数据进行质量评测", - "file_import_evaluation_model_label": "质量评测模型", - "file_import_evaluation_model_placeholder": "请选择质量评测模型", - "file_import_confirm": "确认", - "manage_dimension": "管理维度", - "selected_count": "已选", - "dimension_config_tip": "维度配置说明", - "custom": "自定义", - "select_model_placeholder": "请选择模型", - "create_new_task_modal": "新建任务", - "task_name_input": "任务名", - "task_name_input_placeholder": "请输入任务名", - "evaluation_app_select": "评测应用", - "evaluation_app_support_tip": "当前支持简易应用和工作流,暂不支持插件。", - "evaluation_app_select_placeholder": "请选择评测应用", - "evaluation_app_version_select": "评测应用版本", - "evaluation_app_version_select_placeholder": "请选择评测应用版本", - "evaluation_dataset_select": "评测数据集", - "evaluation_dataset_select_placeholder": "请选择评测数据集", - "create_import_dataset": "新建/导入", - "evaluation_dimensions_label": "评测维度", - "evaluation_dimensions_recommendation": "评测应用包含知识库搜索和AI对话环节,推荐使用 {{num}} 个维度进行评估", - "builtin_dimension": "内置", - "custom_dimension": "自定义", - "config_params": "配置参数", - "score_aggregation_method": "分数聚合方式", - "score_aggregation_method_tip": "选择如何聚合多个维度的评分", - "evaluation_dimensions": "评测维度", - "dimension": "维度", - "judgment_threshold": "判定阈值", - "judgment_threshold_tip": "用于判定测试数据在单个维度中表现是否符合预期,分数低于阈值时,将标记为不符合预期,可辅助识别存在问题的数据,分值为 1~100。\n评测后查看任务时可根据需要再次调整。", - "comprehensive_score_weight": "综合评分权重", - "comprehensive_score_weight_tip": "将按照指定权重计算测试数据全部维度的综合评分,可根据应用使用场景所关注的维度进行设置。\n评测后查看任务时可根据需要再次调整。", - "comprehensive_score_weight_sum": "综合评分权重和:", - "intelligent_generation_dataset": "智能生成数据集", - "dataset_name_input": "名称", - "dataset_name_input_placeholder": "请输入数据集名称", - "generation_basis": "生成依据", - "select_knowledge_base": "选择知识库", - "data_amount": "数据量", - "generation_model": "生成模型", - "generation_model_placeholder": "请选择生成模型", - "high_quality": "质量高", - "needs_improvement": "待优化", - "detail_evaluating": "评测中", - "abnormal": "评测异常", - "not_evaluated": "未评测", - "modify_result": "修改结果", - "restart_evaluation": "重新评测", - "evaluation_error_message": "评测过程中出现异常,请重新评测", - "high_quality_feedback": "该问题质量较高,表述清晰准确,符合标准要求。问题描述完整,答案准确且具有实用性。", - "needs_improvement_feedback": "该问题相对清晰,但可能需要进一步优化。建议增加更多上下文信息,使问题更加具体和明确,以便提供更准确的答案。", - "evaluation_service_error": "评测服务异常", - "edit_data": "编辑数据", - "enter_question": "请输入问题", - "question_required": "问题不能为空", - "reference_answer": "参考答案", - "enter_reference_answer": "请输入参考答案", - "reference_answer_required": "参考答案不能为空", - "quality_evaluation": "质量评测", - "cancel": "取消", - "save": "仅保存", - "save_and_next": "保存并下一个", - "manually_calibrated": "已人工修改结果", - "modify_evaluation_result_title": "修改评测结果", - "evaluation_result_label": "评测结果", - "modify_reason_label": "修改理由", - "modify_reason_input_placeholder": "修改理由", - "no_data": "暂无数据", - "search": "搜索", - "settings": "设置", - "add_data": "追加数据", - "ai_generate": "智能生成", - "manual_add": "手动新增", - "no_answer": "暂无答案", - "confirm_delete_data": "确认删除该数据?", - "no_evaluation_result_click": "还没有测评结果,点击", - "start_evaluation_action": "开始测评", - "evaluation_dataset": "评测数据集", - "evaluation_status": "评测状态", - "manually_add_data_modal": "手动新增数据", - "question_input_label": "问题", - "max_chars_3000_placeholder": "最多 3000 字", - "reference_answer_input_label": "参考答案", - "auto_quality_eval_after_add": "数据质量评测", - "auto_quality_eval_add_tip": "开启后,将自动对数据的问答相关度、合理性等进行评测,给出数据质量评价及原因,可根据评价对数据进行优化。", - "quality_eval_model_label": "质量评测模型", - "select_quality_eval_model_placeholder": "请选择质量评测模型", - "confirm": "确认", - "confirm_quality_evaluation": "确认对全部数据({{total}})进行质量评测吗?", - "model_change_notice": "更改模型后将对后续评测的任务生效。", - "evaluation_model": "评测模型", - "select_evaluation_model": "请选择评测模型", - "evaluation_abnormal": "评测异常", - "error_message": "报错信息", - "file_parse_error": "文件解析异常", - "delete_file": "删除文件", - "reparse": "重新解析", - "data_generation_error": "条数据生成异常", - "source_knowledge_base": "依据知识库", - "source_chunk": "依据分块", - "operations": "操作", - "retry": "重试", - "retry_all": "全部重试", - "error_info": "异常信息", - "manual_add_data": "手动新增数据", - "max_3000_chars": "最多 3000 字", - "please_enter_question": "请输入问题", - "question_max_3000_chars": "问题不能超过3000字", - "please_enter_reference_answer": "请输入参考答案", - "reference_answer_max_3000_chars": "参考答案不能超过3000字", - "auto_quality_evaluation": "新增后自动进行数据质量评测", - "quality_evaluation_tip": "将对数据的问答相关度、合理性等进行评测,给出数据质量评价及原因,可根据评价对数据进行优化。", - "quality_evaluation_model": "质量评测模型", - "please_select_evaluation_model": "请选择质量评测模型", - "summary_pending": "Pending", + "all_apps": "All apps", + "search_evaluation_task": "Task name, app version", + "create_new_task": "Create task", + "task_name_column": "Task name", + "progress_column": "Progress", + "evaluation_app_column": "App", + "app_version_column": "App version", + "evaluation_result_column": "Evaluation result", + "start_finish_time_column": "Time started/completed", + "executor_column": "Operator", + "waiting": "Waiting", + "evaluating_status": "Evaluating", + "completed_status": "Completed", + "queuing_status": "Waiting", + "running_status": "Ongoing", + "error_data_tooltip": "{{count}} entries encountered execution error. Click to view error details.", + "rename": "Rename", + "delete": "Delete", + "confirm_delete_task": "Are you sure you want to delete this task?", + "evaluation_tasks_tab": "Tasks", + "evaluation_tasks": "Evaluation tasks", + "evaluation_datasets_tab": "Datasets", + "evaluation_dimensions_tab": "Metrics", + "create_new": "Create", + "retry_error_data": "Retry failed entries", + "dataset_name_placeholder": "Name", + "create_new_dataset": "Create", + "smart_generation": "Auto generate", + "file_import": "Import from file", + "confirm_delete_dataset": "Are you sure you want to delete this dataset?", + "error_details": "Details", + "status_queuing": "Waiting", + "status_parsing": "Parsing file", + "status_generating": "Generating data", + "status_generate_error": "Generation error", + "status_ready": "Ready", + "status_parse_error": "Parsing error", + "click_to_view_details": "View details", + "table_header_name": "Name", + "table_header_data_count": "Data size", + "table_header_time": "Time created/updated", + "table_header_status": "Status", + "table_header_creator": "Creator", + "create_dimension": "Create", + "search_dimension": "Metric", + "delete_failed": "Operation failed.", + "delete_success": "Deleted successfully.", + "builtin": "Predefined", + "confirm_delete_dimension": "Are you sure that you want to delete this metric?", + "dimension_name": "Name", + "description": "Description", + "create_update_time": "Time created/updated", + "creator": "Creator", + "all": "All", + "app": "App", + "citation_template": "Template", + "correctness": "Correctness", + "conciseness": "Conciseness", + "harmfulness": "Harmfulness", + "controversiality": "Controversiality", + "creativity": "Creativity", + "criminality": "Criminality", + "depth": "Depth", + "details": "Detail", + "dimension_name_label": "Name", + "dimension_description_label": "Description", + "prompt_label": "Prompt", + "citation_template_button": "Template", + "test_run_title": "Trial run", + "question_label": "Question", + "question_placeholder": "", + "answer_label": "Answer", + "reference_answer_label": "Answer for reference", + "reference_answer_placeholder": "", + "actual_answer_label": "Actual answer", + "actual_answer_placeholder": "", + "run_result_label": "Running result", + "start_run_button": "Run", + "running_text": "Running", + "run_success": "Run successful", + "run_failed": "Run failed", + "not_run": "Not run", + "score_unit": "(Score)", + "error_info_label": "Error: ", + "no_feedback_text": "No feedback available.", + "dimension_create_back": "Exit", + "dimension_create_test_run": "Trial run", + "dimension_create_confirm": "OK", + "dimension_create_success": "Created successfully.", + "dimension_create_name_required": "Name is required.", + "dimension_create_prompt_required": "Prompt is required.", + "dimension_get_data_failed": "Failed to obtain the metric data.", + "dimension_data_not_exist": "The metric data does not exist.", + "dimension_update_success": "Update successful.", + "dimension_update_failed": "Update failed.", + "dimension_name_required": "Name is required.", + "dimension_back": "Exit", + "dimension_test_run": "Trial run", + "dimension_save": "Save", + "file_import_back": "Exit", + "file_import_name_label": "Dataset name", + "file_import_name_placeholder": "", + "file_import_select_file": "Please select a file.", + "file_import_success": "File imported successfully.", + "file_import_file_label": "File", + "file_import_download_template": "Download template", + "file_import_download_template_tip": "Download the template to see the required file format.", + "file_import_auto_evaluation_label": "Automatically evaluate the quality of imported data", + "file_import_auto_evaluation_tip": "If enabled, a quality evaluation will be automatically performed on the imported data.", + "file_import_evaluation_model_label": "Evaluation model", + "file_import_evaluation_model_placeholder": "Select", + "file_import_confirm": "OK", + "manage_dimension": "Settings", + "selected_count": "Selected", + "dimension_config_tip": "Dimension configuration instructions", + "custom": "Custom", + "select_model_placeholder": "Select", + "create_new_task_modal": "Create task", + "task_name_input": "Name", + "task_name_input_placeholder": "", + "evaluation_app_select": "App", + "evaluation_app_support_tip": "You can select a simple app or workflow but not a plugin.", + "evaluation_app_select_placeholder": "Select", + "evaluation_app_version_select": "App version", + "evaluation_app_version_select_placeholder": "Select", + "evaluation_dataset_select": "Dataset", + "evaluation_dataset_select_placeholder": "Select", + "create_import_dataset": "Create/Import", + "evaluation_dimensions_label": "Metrics", + "evaluation_dimensions_recommendation": "The evaluation involves knowledge base queries and AI chats. It is recommended to use {{num}} metrics for evaluation.", + "builtin_dimension": "Predefined", + "custom_dimension": "Custom", + "config_params": "Settings", + "score_aggregation_method": "Score aggregation", + "evaluation_dimensions": "Metrics", + "dimension": "Metric", + "judgment_threshold": "Threshold", + "judgment_threshold_tip": "Determines whether a metric's score meets expectations. If it is lower than the specified threshold, it will be marked as Below expectation. Valid range: 1-100. After the evaluation is complete, you can adjust it as needed.", + "comprehensive_score_weight": "Weight", + "comprehensive_score_weight_tip": "The ration of a metric's score to the total score. You can configure it as needed. After the evaluation is complete, you can adjust it as needed.", + "comprehensive_score_weight_sum": "Total weight:", + "intelligent_generation_dataset": "Auto generate dataset", + "dataset_name_input": "Name", + "dataset_name_input_placeholder": "Dataset name", + "generation_basis": "Data source", + "select_knowledge_base": "Select knowledge base", + "data_amount": "Data size", + "generation_model": "Model", + "generation_model_placeholder": "Select", + "high_quality": "High quality", + "needs_improvement": "Pending optimization", + "detail_evaluating": "Evaluating", + "abnormal": "Evaluation error", + "not_evaluated": "Not evaluated", + "modify_result": "Modify", + "restart_evaluation": "Evaluate again", + "evaluation_error_message": "Error occurred during the evaluation. Please try again.", + "high_quality_feedback": "The question is complete and clear, meeting the standard. The answer is accurate and practical.", + "needs_improvement_feedback": "The question needs to be optimized. To obtain a more accurate answer, please provide more context to make the question more specific and clear.", + "evaluation_service_error": "Evaluation encountered error.", + "edit_data": "Edit data", + "enter_question": "", + "question_required": "Question is required.", + "reference_answer": "Answer for reference", + "enter_reference_answer": "", + "reference_answer_required": "Answer for reference is required.", + "quality_evaluation": "Auto quality evaluation", + "cancel": "Cancel", + "save": "Save", + "save_and_next": "Save and proceed", + "manually_calibrated": "Results modified manually.", + "modify_evaluation_result_title": "Modify result", + "evaluation_result_label": "Evaluation result", + "modify_reason_label": "Reason", + "modify_reason_input_placeholder": "Reason", + "no_data": "No data available.", + "search": "Search", + "settings": "Settings", + "add_data": "Add data", + "ai_generate": "Auto generate", + "manual_add": "Manually add", + "no_answer": "No answers available.", + "confirm_delete_data": "Are you sure you want to delete this entry?", + "no_evaluation_result_click": "No evaluation results available. ", + "start_evaluation_action": "start evaluation.", + "evaluation_dataset": "Dataset", + "evaluation_status": "Status", + "manually_add_data_modal": "Add data manually", + "question_input_label": "Question", + "max_chars_3000_placeholder": "Maximum: 3,000 characters", + "reference_answer_input_label": "Answer for reference", + "auto_quality_eval_after_add": "Data quality evaluation", + "auto_quality_eval_add_tip": "If enabled, Q&A relevance and correctness are automatically evaluated, with quality feedback for optimization.", + "quality_eval_model_label": "Quality evaluation model", + "select_quality_eval_model_placeholder": "Select", + "confirm": "OK", + "confirm_quality_evaluation": "Are you sure you want to evaluate the quality of all data ({{total}})?", + "model_change_notice": "Changing the model will apply to subsequent tasks.", + "evaluation_model": "Model", + "select_evaluation_model": "Select", + "evaluation_abnormal": "Evaluation error", + "error_message": "Error message", + "file_parse_error": "File parsing error", + "delete_file": "Delete file", + "reparse": "Reparse", + "data_generation_error": "Generation error", + "source_knowledge_base": "Based on knowledge base", + "source_chunk": "Based on chunk", + "operations": "Operation", + "retry": "Retry", + "retry_all": "Retry all", + "error_info": "Error", + "manual_add_data": "Add data manually", + "max_3000_chars": "Maximum: 3,000 characters", + "please_enter_question": "", + "question_max_3000_chars": "Cannot exceed 3,000 characters.", + "please_enter_reference_answer": "", + "reference_answer_max_3000_chars": "Cannot exceed 3,000 characters.", + "auto_quality_evaluation": "Automatically evaluate data quality of the added data.", + "quality_evaluation_tip": "Q&A relevance and correctness are automatically evaluated, with quality feedback for optimization.", + "quality_evaluation_model": "Quality evaluation model", + "please_select_evaluation_model": "Select", + "summary_pending": "To be generated", "summary_generating": "Generating", "summary_done": "Completed", "summary_failed": "Failed", - "method_mean": "Mean", + "method_mean": "Average", "method_median": "Median", - "prompt_cannot_be_empty": "提示词不允许为空", - "please_select_model": "请选择模型", - "run_failed_please_retry": "运行失败,请重试", - "intelligent_generate_data": "智能生成数据", - "evaluation_completed": "评估完成", - "generation_error": "生成异常", - "data_generating": "数据生成中", - "delete_dataset_error": "删除数据集异常", - "create_failed": "创建失败", - "no_dimension_data": "暂无维度数据", - "please_select_dimension_first": "请先选择该维度", - "model_evaluation_tip": "语言模型可判断实际回答和参考答案中的文本内容是否匹配;\n索引模型可将实际回答和参考答案转成向量,进一步评估语义相似性。", - "create_new_dimension": "新建维度", - "retry_success": "重试成功", - "data_generation_error_count": "{{count}}条数据生成异常" -} + "prompt_cannot_be_empty": "Prompt is required.", + "please_select_model": "Please select a model.", + "run_failed_please_retry": "Failed to run. Please try again.", + "intelligent_generate_data": "Auto generate data", + "evaluation_completed": "Evaluation completed.", + "generation_error": "Generation error", + "data_generating": "Generating data", + "delete_dataset_error": "Error occurred while deleting dataset.", + "create_failed": "Creation failed.", + "no_dimension_data": "No metric data available.", + "please_select_dimension_first": "Please select the metric first.", + "model_evaluation_tip": "The language model determines whether the actual answer and the answer for reference match. The index model converts the actual and reference answers into vectors to further evaluate semantic similarity.", + "create_new_dimension": "Add metric", + "retry_success": "Retry successfully", + "data_generation_error_count": "{{count}} pieces of data generated exception", + "builtin_answer_correctness_name": "Answer Correctness", + "builtin_answer_correctness_desc": "Evaluates the factual consistency between the generated answer and the reference answer, evaluating whether it is accurate and error-free.", + "builtin_answer_similarity_name": "Answer Similarity", + "builtin_answer_similarity_desc": "Evaluates the semantic alignment between the generated answer and the reference answer, determining whether they convey the same core information.", + "builtin_answer_relevancy_name": "Answer Relevance", + "builtin_answer_relevancy_desc": "Evaluates how well the generated answer aligns with the question, judging whether the response directly addresses the query.", + "builtin_faithfulness_name": "Faithfulness", + "builtin_faithfulness_desc": "Evaluates whether the generated answer remains faithful to the provided context, determining whether it contains fabricated or inaccurate content.", + "builtin_context_recall_name": "Context Recall", + "builtin_context_recall_desc": "Evaluates whether the retrieval system successfully retrieves all key information necessary for formulating the answer, assessing the completeness of retrieval.", + "builtin_context_precision_name": "Context Precision", + "builtin_context_precision_desc": "Evaluates whether high-value information is prioritized in the retrieved content, reflecting the quality of ranking and information density.", + "join_evaluation_dataset": "Add to dataset", + "not_join_evaluation_dataset": "Do not add to dataset", + "create_new_dataset_btn_text": "Add to dataset", + "please_select_evaluation_dataset": "Select", + "join_knowledge_base": "Add to knowledge base", + "all_data_with_count": "All data ({{num}})", + "question_data_with_count": "Problem data({{num}})", + "error_data_with_count": "Error data({{num}})", + "export_data": "Export", + "retry_action": "Retry", + "basic_info": "Basics", + "application": "App", + "version": "Version", + "evaluation_dataset_name": "Datasets", + "start_time": "Start time", + "end_time": "End time", + "executor_name": "Operator", + "app_with_search_and_chat_recommendation": "评测应用包含知识库搜索和AI对话环节,已推荐使用 3 个维度进行评估", + "app_with_chat_recommendation": "评测应用包含AI对话环节,已推荐使用 1 个维度进行评估", + "app_with_search_recommendation": "评测应用包含知识库搜索环节,已推荐使用 2 个维度进行评估", + "no_dimensions_added": "还没有添加评测维度,", + "click_to_add": "点击添加", + "meets_expectation": "符合预期!", + "below_expectation": "低于预期分数!", + "summary_generation_error": "总结内容生成异常,", + "error_message_prefix": "报错信息:", + "summary_pending_generation": "总结内容待生成", + "summary_generating_content": "总结内容生成中", + "data_with_count": "数据({{data}})", + "search_placeholder": "搜索", + "detail_title": "详情", + "modify_dataset_simultaneously": "同时修改评测数据集", + "retry_button": "重试", + "edit_action": "编辑", + "delete_action": "删除", + "confirm_delete_data_in_task": "确认在当前任务中删除该数据?", + "view_full_response": "查看完整响应", + "abnormal_status": "异常", + "question_field": "问题", + "reference_answer_field": "参考答案", + "actual_answer_field": "实际回答", + "no_answer_available": "暂无回答", + "comprehensive_score_title": "综合评分", + "dimension_score_title": "维度评分", + "error_data_calculation_notice": "{{count}} 条数据执行异常,仅使用执行成功的数据来计算分数。", + "regenerate_summary_content": "重新生成总结内容", + "processing_status": "处理中...", + "view_results_after_completion": "完成后可查看评测结果", + "all_data_execution_error": "全部数据执行异常,", + "check_error_details": "请查看异常数据中的详细原因,", + "click_to_retry": "点击重试", + "comprehensive_score_weight_description": "按照指定权重计算测试数据全部维度的综合评分,可根据应用使用场景所关注的维度进行设置。", + "no_data_available": "暂无数据", + "question_column": "问题", + "comprehensive_score_column": "综合评分", + "error_info_column": "异常信息", + "request_failed": "请求失败", + "retry_request_submitted": "重试请求已提交", + "retry_failed": "重试失败", + "save_success": "保存成功", + "save_failed": "保存失败", + "summary_generation_request_submitted": "总结生成请求已提交", + "generate_summary_failed": "生成总结失败", + "export_failed": "导出失败", + "load_failed": "加载失败", + "export_success": "导出成功", + "no_dimension_data_cannot_generate_summary": "暂无维度数据,无法生成总结", + "Task_name": "任务名", + "click_to_download_template": "点击下载该应用的 CSV 模板", + "evaluation_created": "评测任务创建成功" +} \ No newline at end of file diff --git a/packages/web/i18n/en/dashboard_mcp.json b/packages/web/i18n/en/dashboard_mcp.json index dd875b8d9718..5749d4982ff8 100644 --- a/packages/web/i18n/en/dashboard_mcp.json +++ b/packages/web/i18n/en/dashboard_mcp.json @@ -2,7 +2,7 @@ "app_alias_name": "Tool name", "app_description": "App description", "app_name": "App name", - "apps": "Exposed apps", + "apps": "Apps for External Use", "create_mcp": "Create MCP service", "create_mcp_server": "Create service", "delete_mcp_server_confirm_tip": "Are you sure you want to delete this service?", @@ -15,13 +15,13 @@ "mcp_link_way": "Connection method", "mcp_name": "MCP service name", "mcp_server": "MCP services", - "mcp_server_description": "You can select apps exposed externally via the MCP protocol. This feature is in beta test because the MCP protocol is not yet stable.", - "not_sse_server": "SSE integration service is not configured.", + "mcp_server_description": "Enable external access to specific apps via MCP services. This feature is in beta test.", + "not_sse_server": "The SSE integration service is not configured.", "search_app": "App name", - "select_app": "Select app", - "start_use": "Use now", + "select_app": "Select apps", + "start_use": "Details", "tool_name": "Tool name", "tool_name_placeholder": "English name is recommended.", - "tool_name_tip": "Some clients only support English. You can change the tool name to an English one.", - "usage_way": "MCP service usage" + "tool_name_tip": "Some MCP clients only support English. You can change the tool name to an English one.", + "usage_way": "MCP service connection" } diff --git a/packages/web/i18n/en/database_client.json b/packages/web/i18n/en/database_client.json new file mode 100644 index 000000000000..de9523346aac --- /dev/null +++ b/packages/web/i18n/en/database_client.json @@ -0,0 +1,24 @@ +{ + "client_destory_error": "数据库连接客户端销毁失败", + "client_not_found": "数据库连接客户端未正确初始化", + "not_support_databaseType": "不支持的数据库类型", + "not_implemented_databaseType": "未实现的数据库客户端", + "connection_failed": "数据库连接失败,请检查连接配置", + "authentication_failed": "数据库认证失败,请检查用户名和密码以及数据库权限是否正确开放", + "database_not_exist": "数据库不存在,请检查数据库名称是否正确", + "database_port_error": "数据库端口错误,请检查端口是否正确", + "connection_address_error": "连接地址错误,请检查地址是否正确", + "connection_timeout": "数据库连接超时,请检查网络是否通畅", + "connection_refused": "数据库连接被拒绝,请检查数据库是否允许外部连接", + "connection_check_error": "连接测试失败,请检查网络是否通畅", + "connection_lost": "数据库连接被中断,请检查网络是否通畅", + "host_error": "连接地址错误,请检查地址是否正确", + "invalid_table_name": "无效的表名", + "fetch_info_error": "获取数据库信息失败", + "database_config_not_found": "未配置数据库连接信息,请先配置", + "table_not_found": "获取表信息失败,表不存在", + "illeagal_table_info": "非法的表信息,请检查表信息是否正确", + "op_unknown_database_error": "尝试操作您的数据库时发生未知错误,请检查数据库连接是否正常", + "dative_service_error": "连接Dative服务时发生错误,请检查配置是否正确", + "tableNamesDuplicate": "存在多个重复表名" +} \ No newline at end of file diff --git a/packages/web/i18n/en/dataset.json b/packages/web/i18n/en/dataset.json index 4a54a3a8dfb1..41df911e8e60 100644 --- a/packages/web/i18n/en/dataset.json +++ b/packages/web/i18n/en/dataset.json @@ -1,27 +1,27 @@ { "Enable": "Enable", "Select_all": "Select all files", - "add_file": "Add", - "api_file": "API file library", + "add_file": "Add file", + "api_file": "API-based file database", "api_url": "API address", "apidataset_configuration": "Settings", "auto_indexes": "Auto generate supplemental indexes", - "auto_indexes_tips": "Generate extra indexes to improve semantic richness and search accuracy.", - "auto_training_queue": "Enhanced index queuing", + "auto_indexes_tips": "Generate extra indexes using models to improve semantic richness and search accuracy.", + "auto_training_queue": "Enhanced index queue", "backup_collection": "Backup data", "backup_dataset": "Import from backup", "backup_dataset_success": "Backup created successfully.", - "backup_dataset_tip": "Import the CSV file downloaded duing the knowledge base export.", - "backup_mode": "Import backup", - "backup_template_invalid": "Invalid backup file. It must be a CSV file with the first columns being q,a,indexes.", + "backup_dataset_tip": "Import the CSV file obtained from the knowledge base export.", + "backup_mode": "Import from backup", + "backup_template_invalid": "Invalid backup file. It must be a CSV file with the first row containing \"q\" in the first column, \"a\" in the second, and \"indexes\" in the third.", "batch_delete": "Delete", "chunk_max_tokens": "Max chunks", "chunk_process_params": "Chunk settings", "chunk_size": "Chunk size", "chunk_trigger": "Chunking condition", "chunk_trigger_force_chunk": "Force chunking", - "chunk_trigger_max_size": "Text exceeds 70% of the max context length.", - "chunk_trigger_min_size": "Text exceeds", + "chunk_trigger_max_size": "Text length exceeds 70% of the max context length", + "chunk_trigger_min_size": "Text length exceeds", "chunk_trigger_tips": "Chunking is triggered only when the specified condition is met. Otherwise, the full text is stored.", "close_auto_sync": "Are you sure you want to disable auto sync?", "collection.Create update time": "Time created/updated", @@ -38,39 +38,39 @@ "collection_tags": "Collection tag", "common.dataset.data.Input Error Tip": "Error occurred while processing the image dataset:", "common.error.unKnow": "Unknown error.", - "common_dataset": "General knowledge base", - "common_dataset_desc": "Build a knowledge base by importing files, adding web links, or manual input.", + "common_dataset": "General", + "common_dataset_desc": "Build a knowledge base by importing files, adding web links, or entering information manually.", "condition": "Condition", "config_sync_schedule": "Scheduled sync", "confirm_delete_collection": "Confirm to delete {{num }} files?", "confirm_import_images": "Total images: {{num}} | Create", - "confirm_to_rebuild_embedding_tip": "Are you sure you want to switch the index for this knowledge base?\nSwitching the index will re-index all data in the knowledge base, which may take a long time. Please ensure you have sufficient AI points.\n\nUpdate apps linked to this knowledge base to avoid confusion with other index models.", + "confirm_to_rebuild_embedding_tip": "Are you sure you want to switch the index for this knowledge base?\nChanging the index will re-index all data in the knowledge base, which may take a long time. Please ensure you have sufficient credits.\n\nUpdate apps linked to this knowledge base to avoid confusion with other index models.", "core.dataset.Image collection": "Image dataset", "core.dataset.import.Adjust parameters": "Adjust parameters", "custom_data_process_params": "Custom", - "custom_data_process_params_desc": "Custom data processing rules", + "custom_data_process_params_desc": "Use custom data processing rules", "custom_split_char": "Delimiter", - "custom_split_sign_tip": "Use custom delimiters to accurately split data. Best for pre-processed data. Use | to define multiple delimiters. Example: “。 | .” for Chinese and English periods.\nAvoid regex characters such as * () [] {}.", + "custom_split_sign_tip": "Enables chunking accurately based on custom delimiters. This is commonly used for processed data. Define multiple delimiters using the | character. For example, \"。 |.\" indicates both Chinese and English periods.\nAvoid using regex characters such as * () [] {}.", "data_amount": "Data groups: {{dataAmount}}, Index groups: {{indexAmount}}", - "data_error_amount": "Training errors: {{errorAmount}}", - "data_index_image": "Image index", + "data_error_amount": "Training errors: {{errorAmount}} groups", + "data_index_image": "Image indexing", "data_index_num": "Index {{index}}", "data_parsing": "Parsing", "data_process_params": "Data processing parameters", "data_process_setting": "Data processing settings", - "data_uploading": "Uploading data: {{num}}%", + "data_uploading": "Uploading: {{num}}%", "dataset.Chunk_Number": "Chunk ID", "dataset.Completed": "Completed", "dataset.Delete_Chunk": "Delete", "dataset.Edit_Chunk": "Edit", "dataset.Error_Message": "Error message", - "dataset.No_Error": "No error detected.", + "dataset.No_Error": "No errors detected.", "dataset.Operation": "Operation", "dataset.ReTrain": "Retry", "dataset.Training Process": "Training status", - "dataset.Training_Count": "Groups in training: {{count}}", - "dataset.Training_Errors": "Errors: {{count}}", - "dataset.Training_QA": "Q&A pairs in training: {{count}}", + "dataset.Training_Count": "In training: {{count}} groups", + "dataset.Training_Errors": "Errors ({{count}})", + "dataset.Training_QA": "In training: {{count}} Q&A pairs", "dataset.Training_Status": "Training status", "dataset.Training_Waiting": "Waiting for {{count}} groups of data", "dataset.Unsupported operation": "Operation is not supported.", @@ -81,222 +81,275 @@ "download_csv_template": "Download CSV template", "edit_dataset_config": "Edit knowledge base", "empty_collection": "Blank dataset", - "enhanced_indexes": "Index enhancement", + "enhanced_indexes": "Enhanced indexing", "error.collectionNotFound": "Collection not found.", "external_file": "External file database", - "external_file_dataset_desc": "Build a knowledge base via API using external file databases.", - "external_id": "File reading ID", - "external_other_dataset_desc": "Build a knowledge base using external documents (including custom API, Feishu, and Yuque documents).", + "external_file_dataset_desc": "Build a knowledge base via APIs using external file databases.", + "external_id": "File ID", + "external_other_dataset_desc": "Build a knowledge base using external documents (including those from API, Feishu, and Yuque).", "external_read_url": "External preview URL", - "external_read_url_tip": "Configure the reading URL of your file database. Used for user access authentication. You can use the {{fileId}} variable to represent the external file ID.", + "external_read_url_tip": "Configure the URL for reading your file database. Used for user access authentication. Use the {{fileId}} variable to represent the external file ID.", "external_url": "File access URL", "failedToLoadRootDirectories": "Failed to load the root directory.", "failedToLoadSubDirectories": "Failed to load the subdirectory.", - "feishu_dataset": "Feishu", + "feishu_dataset": "Feishu knowledge base", "feishu_dataset_config": "Configure Feishu knowledge base", - "feishu_dataset_desc": "You can build a knowledge base from Feishu documents and set permissions. The documents will not be stored again.", + "feishu_dataset_desc": "Build a knowledge base from Feishu documents by configuring permissions. The documents will not be stored again.", "file_list": "Files", - "file_model_function_tip": "Used to enhance indexes and Q&A generation.", + "file_model_function_tip": "Enhances indexes and Q&A generation.", "filename": "File name", "folder_dataset": "Folder", "getDirectoryFailed": "Failed to obtain the directory.", - "image_auto_parse": "Auto image indexing", - "image_auto_parse_tips": "Use VLM to tag images in documents and generate extra search indexes.", + "image_auto_parse": "Auto index images", + "image_auto_parse_tips": "Automatically mark images in documents using the VLM and generate search indexes for them.", "image_training_queue": "Image processing queue", "images_creating": "Creating", "immediate_sync": "Sync now", "import.Auto mode Estimated Price Tips": "Text understanding model call is required, and the consumed AI points ({{price}} points/1K tokens) is high.", "import.Embedding Estimated Price Tips": "Only index model call is required, and the consumed AI points: ({{price}} points / 1K tokens) is low.", - "import_confirm": "Upload file", - "import_data_preview": "Data preview", - "import_data_process_setting": "Data processing method", + "import_confirm": "Upload files", + "import_data_preview": "Preview data", + "import_data_process_setting": "Data processing", "import_file_parse_setting": "File parsing", "import_model_config": "Select model", - "import_param_setting": "Parameters", - "import_select_file": "Select a file", + "import_param_setting": "Configure parameters", + "import_select_file": "Select files", "import_select_link": "Link", - "index_prefix_title": "Add title to index", - "index_prefix_title_tips": "Auto add titles to all indexes", + "index_prefix_title": "Include titles in indexes", + "index_prefix_title_tips": "Automatically include titles in all indexes.", "index_size": "Index size", - "index_size_tips": "During vectorization, content is automatically split into chunks based on the configured maximum chunk size.", - "input_required_field_to_select_baseurl": "Please specify the required fields first.", + "index_size_tips": "Length of content during vectorization. The system will automatically create indexes for chunks based on the specified index size.", + "input_required_field_to_select_baseurl": "Please configure the required fields first.", "insert_images": "Add image", "insert_images_success": "Image added successfully. It will be displayed only after training is complete.", "is_open_schedule": "Enable scheduled sync", "keep_image": "Retain image", - "llm_paragraph_mode": "Recognize paragraph", + "llm_paragraph_mode": "Paragraph recognition", "llm_paragraph_mode_auto": "Auto", - "llm_paragraph_mode_auto_desc": "Enable model recognition if the text does not contain title in Markdown format.", - "llm_paragraph_mode_forbid": "Disable", - "llm_paragraph_mode_forbid_desc": "Forcibly disable paragraph auto recognition.", - "llm_paragraph_mode_force": "Enable", - "llm_paragraph_mode_force_desc": "Forcibly perform auto paragraph recognition using the model and ignore the original paragraph structure (if any).", + "llm_paragraph_mode_auto_desc": "Automatically enable paragraph recognition if the text does not contain titles in Markdown format.", + "llm_paragraph_mode_forbid": "Disabled", + "llm_paragraph_mode_forbid_desc": "Forcibly disable auto paragraph recognition.", + "llm_paragraph_mode_force": "Enabled", + "llm_paragraph_mode_force_desc": "Forcibly implement auto paragraph recognition using the model and ignore the original paragraph structure (if any).", "loading": "Loading...", "max_chunk_size": "Max chunk size", - "move.hint": "If moved, the selected apps or folders will inherit the permissions of the destination folder, and their current permissions will become invalid.", + "move.hint": "If moved, the knowledge base or folder will inherit the permissions of the destination folder, invalidating its current permissions.", "noChildren": "The subdirectory does not exist.", - "noSelectedFolder": "Please select at least one folder.", - "noSelectedId": "Please select at least one ID.", - "noValidId": "All IDs are invalid.", - "open_auto_sync": "After enabling scheduled synchronization, the system will attempt to synchronize the collection at random times each day. During the synchronization process, data from the collection may not be searchable.", - "other_dataset": "Third-party knowledge bases", + "noSelectedFolder": "Please select a folder first.", + "noSelectedId": "Please select an ID first.", + "noValidId": "No valid IDs available.", + "open_auto_sync": "If scheduled sync is enabled, the system will sync collections at any time every day. Data from the collections cannot be retrieved during the collection sync.", + "other_dataset": "Third-party", "paragraph_max_deep": "Max paragraph depth", - "paragraph_split": "Split by paragraph", - "paragraph_split_tip": "Prioritize chunking by Markdown heading paragraphs; if the chunks are too long, perform secondary chunking by length.", + "paragraph_split": "Chunking by paragraph", + "paragraph_split_tip": "Split text based on Markdown heading structure first. If a chunk exceeds the maximum length, it will be split further.", "params_config": "Configuration", "pdf_enhance_parse": "Enhanced PDF parsing", - "pdf_enhance_parse_price": "{{price}} points/page", - "pdf_enhance_parse_tips": "PDF recognition model supports parsing PDF files, converting PDF files into Markdown format with images preserved, and processing scanned copies of PDF files, which takes a longer time.", - "permission.des.manage": "Manage the entire knowledge base data and information", - "permission.des.read": "Can view knowledge base content", - "permission.des.write": "Can add and modify knowledge base content", - "pleaseFillUserIdAndToken": "User ID and token are required.", + "pdf_enhance_parse_price": "{{price}} credits/page", + "pdf_enhance_parse_tips": "Call a PDF recognition model to parse PDF files, converting them into Markdown format with images preserved, and to process scanned copies of PDF files, which takes longer time.", + "permission.des.manage": "Manages all data and information of an entire knowledge base.", + "permission.des.read": "Views the knowledge base content.", + "permission.des.write": "Adds and modifies the knowledge base content.", + "pleaseFillUserIdAndToken": "User ID and Token are required.", "preview_chunk": "Chunk preview", "preview_chunk_empty": "The file is empty.", - "preview_chunk_folder_warning": "Directory preview not supported.", - "preview_chunk_intro": "{{total}} chunks in total. Up to 10 chunks can be displayed.", - "preview_chunk_not_selected": "Click the file on the left to preview it.", - "process.Auto_Index": "Auto index generation", - "process.Get QA": "Q&A pair extraction", - "process.Image_Index": "Image index generation", + "preview_chunk_folder_warning": "Directory preview is not supported.", + "preview_chunk_intro": "Total chunks: {{total}}, Max displayed: 10", + "preview_chunk_not_selected": "Click a file on the left for preview.", + "process.Auto_Index": "Auto generate indexes", + "process.Get QA": "Extract Q&A pairs", + "process.Image_Index": "Generate image indexes", "process.Is_Ready": "Ready", "process.Is_Ready_Count": "{{count}} groups are ready.", - "process.Parse_Image": "Parsing image...", - "process.Parsing": "Parsing content...", - "process.Vectorizing": "Index vectorization", + "process.Parse_Image": "Parse images", + "process.Parsing": "Parse content", + "process.Vectorizing": "Vectorize indexes", "process.Waiting": "Waiting", - "rebuild_embedding_start_tip": "The task of switching the index model has started.", + "rebuild_embedding_start_tip": "Switch index model", "rebuilding_index_count": "Indexes being rebuilt: {{count}}", "request_headers": "Request header parameter. Bearer will be automatically added.", - "retain_collection": "Adjust training parameter", - "retrain_task_submitted": "Retraining task has been submitted", + "retain_collection": "Adjust training parameters", + "retrain_task_submitted": "The retraining task was submitted.", "retry_all": "Retry all", - "retry_failed": "Operation failed again.", + "retry_failed": "Retry failed.", "rootDirectoryFormatError": "The entries in the root directory are invalid.", "rootdirectory": "/root directory", "same_api_collection": "The API collection already exists.", "selectDirectory": "Select", - "selectRootFolder": "Select", - "split_chunk_char": "By specified delimiter", - "split_chunk_size": "By length", + "selectRootFolder": "Select Root Directory", + "split_chunk_char": "Chunking by delimiter", + "split_chunk_size": "Chunking by length", "split_sign_break": "1 line break", "split_sign_break2": "2 line breaks", "split_sign_custom": "Custom", "split_sign_exclamatiob": "Exclamation mark", - "split_sign_null": "Ignore", + "split_sign_null": "Not specified", "split_sign_period": "Period", "split_sign_question": "Question mark", - "split_sign_semicolon": "semicolon", + "split_sign_semicolon": "Semicolon", "start_sync_dataset_tip": "Are you sure you want to sync the entire knowledge base?", "status_error": "Running error occurred.", - "sync_collection_failed": "Synchronization collection error, please check if the source file can be accessed normally.", + "sync_collection_failed": "Error occurred while syncing the collection. Please check whether the source file is accessible.", "sync_schedule": "Scheduled sync", - "sync_schedule_tip": "Only existing collections will be synced. Including the collection of links and all collections in the API knowledge base. The system performs daily polling updates, and the exact update time cannot be determined.", + "sync_schedule_tip": "Only existing collections will be synced, including link collections and all collections in the API knowledge base. The update time is not fixed because collections are updated based on round-robin scheduling.", "tag.Add_new_tag": "Add tag", "tag.Edit_tag": "Edit tag", - "tag.add": "Create", + "tag.add": "Add tag", "tag.add_new": "Add", "tag.cancel": "Deselect", - "tag.delete_tag_confirm": "Are you sure you want to delete the label?", - "tag.manage": "Tags", + "tag.delete_tag_confirm": "Are you sure you want to delete the tag?", + "tag.manage": "Manage tags", "tag.searchOrAddTag": "Search or add", "tag.tags": "Tag", - "tag.total_tags": "{{total}} tags in total", - "template_dataset": "Import template", - "template_file_invalid": "The template file format is incorrect; it should be a CSV file with the first column as q, a, indexes.", - "template_mode": "Import template", - "the_knowledge_base_has_indexes_that_are_being_trained_or_being_rebuilt": "The knowledge base has indexes that are in training or being rebuilt.", - "total_num_files": "{{total}} files in total", - "training.Error": "{{count}} groups encountered errors.", + "tag.total_tags": "Total tags: {{total}}", + "template_dataset": "Import from template", + "template_file_invalid": "Invalid template file. Must be a CSV file with the top row being q, a, indexes.", + "template_mode": "Import from template", + "the_knowledge_base_has_indexes_that_are_being_trained_or_being_rebuilt": "The knowledge base contains indexes that are being trained or rebuilt.", + "total_num_files": "Total files: {{total}}", + "training.Error": "{{count}} groups encountered error.", "training.Image mode": "Image processing", "training.Normal": "Normal", "training_mode": "Chunk mode", "training_ready": "{count} groups", - "upload_by_template_format": "Upload according to the template file", + "upload_by_template_format": "Please download the template, complete it with your own data, and then upload it.", "uploading_progress": "Uploading: {{num}}%", - "vector_model_max_tokens_tip": "Each chunk of data has Cannot exceed 3000 tokens.", - "vllm_model": "Image understanding model", - "vlm_model_required_warning": "Image understanding model is required.", + "vector_model_max_tokens_tip": "Each chunk cannot exceed 3000 tokens.", + "vllm_model": "VLM", + "vlm_model_required_warning": "A VLM is required.", "website_dataset": "Website sync", - "website_dataset_desc": "Using crawlers Bulk batch scrape web data to build a knowledge base.", - "website_info": "Website information", + "website_dataset_desc": "Build a knowledge base by scraping web data using web crawlers.", + "website_info": "Website info", "yuque_dataset": "Yuque knowledge base", "yuque_dataset_config": "Configure Yuque knowledge base", - "yuque_dataset_desc": "You can configure the permissions of Yuque to build a knowledge base using Yuque, and the documents will not be stored a second time.", - "enterprise_database": "数据库", - "enterprise_database_desc": "授权 MySQL 数据库后读取选定的数据表,检索时连接数据库获取相关信息", - "enterprise_database_embedding_model_tip": "索引模型可将数据库关键信息(截取部分数据的表名、表描述、列名、列描述)转成向量,用于进行语义检索", - "database_structure_change_tip": "数据结构发生变化时,如数据表名称变更或其中的列名变更等,请手动刷新数据源。", - "refresh_data_source": "刷新数据源", - "search_name_or_description": "搜索名称或描述", - "connect_database": "连接数据库", - "data_config": "数据配置", - "database_type": "数据库类型", - "mysql_description": "支持 5.7 及其以上版本", - "database_host": "数据库地址", - "host_placeholder": "请输入IP地址或域名", - "host_tips": "请确保数据库所在与本平台连通,填入可访问的地址", - "host_required": "数据库地址不能为空", - "port": "端口号", - "port_required": "端口号不能为空", - "port_invalid": "端口号格式不正确", - "port_range_error": "端口号必须在1-65535范围内", - "database_name": "数据库名称", - "database_name_placeholder": "请输入数据库名称", - "database_name_required": "数据库名称不能为空", - "database_username": "数据库用户名", - "username_placeholder": "请输入用户名", - "username_required": "用户名不能为空", - "database_password": "数据库密码", - "password_placeholder": "请输入密码", - "password_required": "密码不能为空", - "connection_pool_size": "连接池大小", - "connection_pool_required": "连接池大小不能为空", - "connection_pool_min_error": "连接池大小不能小于1", - "connection_pool_max_error": "连接池大小不能大于100", - "connect_next_step": "连接并下一步", - "database_config_title": "知识库将调用已选数据表中的数据进行索引", - "search_tables": "数据表名", - "table_selection_warning": "存在未选择内容", - "table_description": "数据表描述", - "table_description_placeholder": "默认值是数据表表自带的描述", - "default_table_description": "默认值是数据表表自带的描述", - "column_configuration": "数据列配置", - "search_columns": "列名", - "column_name": "列名", - "column_type": "类型", - "column_description": "描述", - "column_enabled": "启用", - "default_column_description": "默认值是数据表表自带的描述", - "confirm": "确认", - "edit_database_config_warning": "数据源已配置,发生 {{changedCount}} 个数据表存在列的更改,{{deletedCount}} 个数据表已不存在,请修正最新数据。", - "edit_database_warning": "修改数据库信息后,当前正在访问数据库的应用将断开重连,可能会导致正在运行的应用无法检索到结果。", - "database_config": "配置", - "refresh_success": "刷新成功", - "no_data_changes": "未出现信息变更", - "refresh_datasource": "刷新数据源", - "reconnect_success": "重新连接数据库,变更配置成功", - "auth_failed": "数据库连接失败,认证未通过,请检查用户名和密码。", - "connection_failed": "连接失败", - "reconnecting": "重新连接中", - "reconnect_success_detail": "已重新连接数据库,变更配置成功", - "table_changes_notice": "发现 {{changedCount}} 个数据表存在列的变更,{{deletedCount}} 个数据表已不存在,请核查最新数据。", - "reconnect_database": "重新连接数据库", - "column_desc_accuracy_tip": "为了提高问答准确率,请准确填写列描述,用来解释此列数据的含义和用途,大模型将会根据列描述选择对应的列数据进行检索和生成回答", - "default_table_desc_tip": "默认使用数据表中定义的描述。", - "column_enabled_tip": "启用后表示使用该列数据进行检索及回答", - "connecting": "正在连接", - "test_connectivity": "测试连通性", - "connect_and_next": "连接并进行下一步", - "connection_network_error": "连接失败,请检查网络连接", - "validate_ip_tip": "该输入项禁止使用本地回环地址,如:localhost、127.x.x.x、0.0.0.0", - "database": "数据库", - "search_model": "检索模型", - "search_model_desc": "用于生成可在数据库中检索的SQL语句,并进行检索与汇总,生成可用于对话的文本。", - "search_model_tip": "使用非推理模型、参数量大的模型效果更佳。", - "other_knowledge_base": "其他知识库", - "database_search": "数据库检索", - "description": "描述", - "remove": "移除", - "confirm_remove_database_table": "确认移除该数据表及其所有数据配置?" + "yuque_dataset_desc": "Build a knowledge base from Yuque documents by configuring permissions. The documents will not be stored again.", + "enterprise_database": "Database", + "enterprise_database_desc": "Create a knowledge base and connect it to a MySQL database to retrieve related information.", + "enterprise_database_embedding_model_tip": "The index model can convert key database information (such as table name, table description, column name, and column description) into vectors for semantic search.", + "database_structure_change_tip": "When the data structure changes, including changes to the table or column name, please refresh the data sources.", + "refresh_data_source": "Refresh", + "search_name_or_description": "Name, description", + "connect_database": "Connect to database", + "data_config": "Select tables", + "database_type": "Database type", + "mysql_description": "Supported versions: MySQL 5.7 and later", + "database_host": "Database address", + "host_placeholder": "IP address or domain name", + "host_tips": "Please enter a reachable address.", + "host_required": "This field is required.", + "port": "Port", + "port_required": "This field is required.", + "port_invalid": "Port number is invalid.", + "port_range_error": "Port range: 1-65535", + "database_name": "Database name", + "database_name_placeholder": "", + "database_name_required": "Database name is required.", + "database_username": "Username", + "username_placeholder": "", + "username_required": "Username is required.", + "database_password": "Password", + "password_placeholder": "", + "password_required": "Password is required.", + "connection_pool_size": "Connection pool size", + "connection_pool_required": "This field is required.", + "connection_pool_min_error": "Cannot be smaller than 1.", + "connection_pool_max_error": "Cannot exceed 100.", + "connect_next_step": "Connect & proceed", + "database_config_title": "The knowledge base will index data from the selected tables.", + "search_tables": "Table name", + "table_selection_warning": "Configuration is not complete.", + "table_description": "Description", + "table_description_placeholder": "Default value: Predefined description in the table", + "default_table_description": "Default value: Predefined description in the table", + "column_configuration": "Columns", + "search_columns": "Column name", + "column_name": "Column name", + "column_type": "Type", + "column_description": "Description", + "column_enabled": "Status", + "default_column_description": "Default value: Predefined description in the table", + "confirm": "OK", + "edit_database_config_warning": "Data source configuration is complete. {{changedCount}} tables have column changes, and {{deletedCount}} no longer exist. Please refresh the data source.", + "edit_database_warning": "After the database information is modified, apps connecting to it will be disconnected and may fail to retrieve information.", + "database_config": "Settings", + "refresh_success": "Refreshed successfully.", + "no_data_changes": "No information changed.", + "refresh_datasource": "Refresh", + "reconnect_success": "Reconnected to the database. Configuration changed successfully.", + "auth_failed": "Failed to connect to the database because authentication failed. Please check the username and password.", + "connection_failed": "Connection failed.", + "reconnecting": "Reconnecting...", + "reconnect_success_detail": "Reconnected to the database. Configuration changed successfully.", + "table_changes_notice": "{{changedCount}} data tables have column changes, and {{deletedCount}} no longer exist. Please update the data.", + "reconnect_database": "Reconnect to database", + "column_desc_accuracy_tip": "To increase the accuracy of answers, please accurately describe the meaning and purpose of the data in a column. The model will select the columns to retrieve information and generate answers based on the column description.", + "default_table_desc_tip": "The predefined description in the data table is used by default.", + "column_enabled_tip": "If enabled, the model will query this column to retrieve data for generating answers.", + "connecting": "Connecting", + "test_connectivity": "Test connectivity", + "connect_and_next": "Connect & proceed", + "connection_network_error": "Connection failed. Please check network connectivity.", + "validate_ip_tip": "Loopback addresses (such as localhost, 127.x.x.x, and 0.0.0.0) are not allowed.", + "database": "Database", + "search_model": "Search model", + "search_model_desc": "Generates SQL statements for database search, performs search and aggregation, and produces chat text.", + "search_model_tip": "Larger non-reasoning models generally generate better results.", + "other_knowledge_base": "Other knowledge bases", + "database_search": "Database search", + "description": "Description", + "remove": "Remove", + "confirm_remove_database_table": "Are you sure you want to remove this table and all its configuration?", + "data_source_refreshed": "数据源已刷新,", + "found": "发现", + "tables_with_column_changes": "发现 {{modifiedTablesCount}} 个数据表存在列的变更,", + "tables_not_exist": "发现 {{delTablesCount}} 个数据表已不存在,", + "please_check_latest_data": "请核查最新数据", + "has_unfilled_content": "存在未填写的内容", + "has_column_changes": "存在列的变更", + "connection_success": "连接成功", + "no_changes_detected": "未出现信息变更。", + "tables_added": "新增 {{addedTables}} 个数据表", + "tables_modified": "发现 {{modifiedTables}} 个数据表存在列的变更,", + "tables_deleted": "发现 {{deletedTables}} 个数据表已不存在,", + "please_verify_data": "请核查最新数据。", + "changes_detected": "发现", + "new_columns_added_disabled": "个新增列(默认未启用),", + "columns_no_longer_exist": "个列已不存在,", + "check_latest_data": ",请核查最新数据。", + "config": "配置", + "refresh_failed": "刷新失败", + "unknown_error": "未知错误", + "no_data_available": "暂无数据", + "database_sql_query": "数据库检索的 SQL 语句", + "search_result": "检索结果", + "no_search_result": "暂无检索结果", + "database_address_required": "数据库地址不能为空", + "ip_format_invalid_range": "IP地址格式不正确,每段数值应在0-255之间", + "database_address_format_invalid": "数据库地址格式不正确,请输入有效的IP地址或域名", + "data_index_column_value":"SQL字段示例值索引", + "error_create_datasetcollection": "创建数据集失败,请联系管理员", + "tables_modified_and_deleted": "发现 {{modifiedTables}} 个数据表存在列的变更,{{deletedTables}} 个数据表已不存在,", + "tables_no_longer_exist_comma": "个数据表已不存在,", + "process.databaseSchema": "数据库结构索引生成", + "database_dataset_desc": "直连企业内部MySQL、SQLite等数据库构建知识库", + "database_connection_config": "数据库连接配置", + "database_url": "数据库地址", + "database_url_placeholder": "例如: localhost 或 192.168.1.100", + "database_url_required": "数据库地址不能为空", + "database_port": "端口", + "database_username_placeholder": "数据库用户名", + "database_username_required": "用户名不能为空", + "database_password_placeholder": "数据库密码", + "database_password_required": "密码不能为空", + "database_max_connections": "最大连接数", + "database_config_description": "配置数据库连接信息,系统将直连您的数据库进行数据查询和向量化处理。", + "database_required_fields": "请填写所有必填字段", + "set_database_config": "配置数据库连接", + "save_database_config": "保存配置", + "confirm_update_database_config": "确认更新数据库配置?", + "confirm_create_database_config": "确认创建数据库配置?", + "database_config_updated": "数据库配置已更新", + "data_index_column_description": "SQL字段描述索引", + "no_data_in_database": "该数据库中没有数据" } + diff --git a/packages/web/i18n/en/evaluation.json b/packages/web/i18n/en/evaluation.json index 526e03bf654b..9f2d033145f2 100644 --- a/packages/web/i18n/en/evaluation.json +++ b/packages/web/i18n/en/evaluation.json @@ -1,128 +1,127 @@ { "dataset_collection_not_found": "Evaluation dataset collection not found", - "dataset_data_not_found": "Evaluation dataset data not found", - "name_required": "Evaluation task name is required", - "name_too_long": "Evaluation task name is too long", - "description_too_long": "Evaluation task description is too long", - "dataset_id_required": "Dataset ID is required", - "target_required": "Evaluation target is required", - "target_invalid_config": "Evaluation target configuration is invalid", - "target_app_id_missing": "Application ID is missing", - "target_version_id_missing": "Application version ID is missing", - "evaluators_required": "Evaluators are required", - "evaluator_invalid_config": "Evaluator configuration is invalid", + "dataset_data_not_found": "Evaluation dataset not found.", + "name_required": "Name is required.", + "name_too_long": "Name is too long.", + "description_too_long": "Description is too long.", + "target_required": "Evaluation target is required.", + "target_invalid_config": "Evaluation target configuration is invalid.", + "target_app_id_missing": "App ID is missing.", + "target_version_id_missing": "App version ID is missing.", + "evaluators_required": "Evaluator is required.", + "evaluator_invalid_config": "Evaluator configuration is invalid.", "evaluator_invalid_score_scaling": "Invalid score scaling value, must be a positive number greater than 0 and less than or equal to 10000 (decimals supported, e.g., 0.01 means 100x reduction)", - "user_input_required": "User input is required", - "expected_output_required": "Expected output is required", - "invalid_format": "Invalid format", - "id_required": "Evaluation ID is required", - "item_id_required": "Evaluation item ID is required", - "data_item_id_required": "Data item ID is required", - "invalid_context": "Invalid context", - "invalid_retrieval_context": "Invalid retrieval context", - "insufficient_permission": "Insufficient permission", - "app_not_found": "Application not found", - "task_not_found": "Evaluation task not found", - "item_not_found": "Evaluation item not found", + "user_input_required": "User input is required.", + "expected_output_required": "Expected output is required.", + "invalid_format": "Format is invalid.", + "id_required": "Evaluation ID is required.", + "item_id_required": "Evaluation item ID is required.", + "data_item_id_required": "Item ID is required.", + "invalid_context": "Context is invalid.", + "invalid_retrieval_context": "Search context is invalid.", + "insufficient_permission": "You do not have permission.", + "app_not_found": "App not found.", + "task_not_found": "Evaluation task not found.", + "item_not_found": "Evaluation item not found.", "metric_builtin_cannot_modify": "Built-in metrics cannot be modified", "metric_builtin_cannot_delete": "Built-in metrics cannot be deleted", - "invalid_status": "Invalid status", - "invalid_state_transition": "Invalid state transition, Only queued or manually stopped evaluations can be started", - "only_running_can_stop": "Only running evaluations can be stopped", - "only_failed_can_retry": "Only failed evaluation items can be retried", - "item_no_error_to_retry": "Evaluation item has no error to retry", - "target_output_required": "Target output is required", - "evaluator_output_required": "Evaluator output is required", - "dataset_load_failed": "Dataset loading failed", - "target_config_invalid": "Target configuration is invalid", - "evaluators_config_invalid": "Evaluators configuration is invalid", - "unsupported_target_type": "Unsupported target type", - "app_version_not_found": "Application version not found", - "duplicate_dataset_name": "Duplicate dataset name", + "invalid_status": "Status is invalid.", + "invalid_state_transition": "Invalid status transition. Only evaluations in queued or manually stopped status can be started.", + "only_running_can_stop": "Only running evaluations can be stopped.", + "item_no_error_to_retry": "Retry is not available because no failed evaluation items.", + "target_execution_error": "Target Execution error.", + "dataset_load_failed": "Failed to load dataset.", + "target_config_invalid": "Target configuration is invalid.", + "evaluators_config_invalid": "Evaluator configuration is invalid.", + "unsupported_target_type": "Target type is not supported.", + "app_version_not_found": "App version not found.", + "duplicate_dataset_name": "The dataset name already exists.", "no_data_in_collections": "No data in collections", - "update_failed": "Update failed", - "lock_acquisition_failed": "Lock acquisition failed", + "update_failed": "Update failed.", + "task_system_error": "System error occurred while processing evaluation task", + "manually_stopped": "Manually stopped", + "evaluator_execution_errors": "Evaluator execution errors", - "metric_not_found": "Evaluation metric not found", - "metric_un_auth": "No permission to operate this evaluation metric", - "metric_name_required": "Metric name is required and must be a non-empty string", - "metric_name_too_long": "Metric name must be less than 100 characters", - "metric_description_too_long": "Metric description must be less than 100 characters", - "metric_prompt_required": "Metric prompt is required and must be a non-empty string", - "metric_prompt_too_long": "Metric prompt must be less than 4000 characters", - "metric_type_required": "Metric type is required", - "metric_type_invalid": "Invalid metric type", - "metric_id_required": "Missing required parameter: metricId", + "metric_not_found": "Evaluation metrics not found.", + "metric_un_auth": "You do not have permission to operate this evaluation metric.", + "metric_name_required": "Metric name is required.", + "metric_name_too_long": "Metric name cannot exceed 100 characters.", + "metric_description_too_long": "Metric description cannot exceed 100 characters.", + "metric_prompt_required": "Metric prompt is required.", + "metric_prompt_too_long": "Metric prompt cannot exceed 4,000 characters.", + "metric_type_required": "Metric type is required.", + "metric_type_invalid": "Metric type is invalid.", + "metric_name_invalid": "Metric name is invalid.", + "metric_id_required": "Metric ID is required.", - "eval_case_required": "EvalCase is required", - "eval_case_user_input_required": "UserInput is required and must be a non-empty string", - "eval_case_user_input_too_long": "UserInput must be less than 1000 characters", - "eval_case_actual_output_required": "ActualOutput is required and must be a non-empty string", - "eval_case_actual_output_too_long": "ActualOutput must be less than 4000 characters", - "eval_case_expected_output_required": "ExpectedOutput is required and must be a non-empty string", - "eval_case_expected_output_too_long": "ExpectedOutput must be less than 4000 characters", + "eval_case_required": "Evaluation case is required.", + "eval_case_user_input_required": "User input is required.", + "eval_case_user_input_too_long": "User input cannot exceed 1,000 characters.", + "eval_case_actual_output_required": "Actual output is required.", + "eval_case_actual_output_too_long": "Actual output cannot exceed 4,000 characters.", + "eval_case_expected_output_required": "Expected output is required.", + "eval_case_expected_output_too_long": "Expected output cannot exceed 4,000 characters.", - "llm_config_required": "LLM config is required", - "llm_model_name_required": "LLM model name is required and must be a non-empty string", + "llm_config_required": "LLM configuration is required.", + "llm_model_name_required": "LLM model name is required.", - "debug_evaluation_failed": "Evaluation debug failed", + "debug_evaluation_failed": "Failed to debug evaluation.", - "evaluator_config_required": "Evaluator configuration is required", - "evaluator_llm_config_missing": "Evaluator requires LLM configuration but it is missing", - "evaluator_embedding_config_missing": "Evaluator requires embedding model configuration but it is missing", - "evaluator_llm_model_not_found": "LLM model not found or failed to get", - "evaluator_embedding_model_not_found": "Embedding model not found or failed to get", - "evaluator_request_timeout": "Evaluator request timeout", - "evaluator_service_unavailable": "Evaluator service unavailable", - "evaluator_invalid_response": "Evaluator returned invalid response", - "evaluator_network_error": "Evaluator network connection error", - - "eval_id_required": "Evaluation task ID is required", - "summary_metrics_config_error": "Metrics configuration error", - "summary_threshold_value_required": "Threshold value is required", - "summary_weight_required": "Weight is required", - "summary_weight_must_be_number": "Weight must be a number", - "summary_threshold_must_be_number": "Threshold value must be a number", - "summary_calculate_type_required": "Calculate type is required", - "summary_calculate_type_invalid": "Calculate type is invalid", - "summary_no_valid_metrics_found": "No valid metrics found", - "summary_stream_response_not_supported": "Stream response not supported", - "summary_weight_sum_must_be_100": "The sum of all metric weights must equal 100", + "evaluator_config_required": "Evaluator configuration is required.", + "evaluator_llm_config_missing": "LLM configuration is missing in evaluator.", + "evaluator_embedding_config_missing": "Embedding model configuration is missing in evaluator.", + "evaluator_llm_model_not_found": "LLM model does not exist or failed to load.", + "evaluator_embedding_model_not_found": "Embedding model does not exist or failed to load.", + "evaluator_request_timeout": "Evaluator request timed out.", + "evaluator_service_unavailable": "Evaluator service is unavailable.", + "evaluator_invalid_response": "An invalid response was returned by the evaluator.", + "evaluator_network_error": "Evaluator network connection error.", - "dataset_collection_id_required": "Dataset collection ID is required", - "dataset_collection_update_failed": "Dataset collection update failed", - "dataset_model_not_found": "model not found", - "dataset_no_data": "Dataset contains no data", - "dataset_data_id_required": "Dataset data ID is required", - "data_quality_status_invalid": "Invalid data quality status", - "dataset_data_user_input_required": "User input is required", - "dataset_data_expected_output_required": "Expected output is required", - "dataset_data_actual_output_must_be_string": "Actual output must be a string", - "dataset_data_context_must_be_array_of_strings": "Context must be an array of strings", - "dataset_data_retrieval_context_must_be_array_of_strings": "Retrieval context must be an array of strings", - "dataset_data_enable_quality_eval_required": "Enable quality evaluation flag is required", - "dataset_data_evaluation_model_required_for_quality": "Evaluation model is required when quality evaluation is enabled", - "dataset_data_metadata_must_be_object": "Metadata must be an object", - "dataset_data_list_error": "Failed to list dataset data", - "quality_assessment_failed": "Quality assessment failed", - "data_quality_job_active_cannot_set_high_quality": "Cannot set quality status to high quality while quality assessment job is active", - "dataset_task_not_retryable": "Task is not retryable", - "dataset_task_job_not_found": "Task job not found", - "dataset_task_job_mismatch": "Task job does not belong to this collection", - "dataset_task_only_failed_can_delete": "Only failed tasks can be deleted", - "dataset_task_operation_failed": "Task operation failed", - "dataset_task_delete_failed": "Dataset task deletion failed", - "fetch_failed_tasks_error": "Failed to fetch failed tasks list", - "file_id_required": "File ID is required", - "file_must_be_csv": "File must be a CSV file", - "csv_invalid_structure": "CSV file has invalid structure", - "csv_too_many_rows": "CSV file contains too many rows (maximum 10,000)", - "csv_parsing_error": "CSV parsing error", - "csv_no_data_rows": "CSV file contains no data rows", - "count_must_be_greater_than_zero": "Count must be greater than zero", - "count_exceeds_available_data": "Requested count exceeds available data", - "selected_datasets_contain_no_data": "Selected datasets contain no data", - "model_name_invalid": "Evaluation model name must be a string", - "model_name_too_long": "Evaluation model name must be less than 100 characters", - "description_invalid_type": "Description must be a string" + "eval_id_required": "Evaluation task ID is required.", + "summary_metrics_config_error": "Metric configuration error.", + "summary_threshold_value_required": "Threshold is required.", + "summary_weight_required": "Weight is required.", + "summary_weight_must_be_number": "The weight must be a digit.", + "summary_threshold_must_be_number": "The threshold must be a digit.", + "summary_calculate_type_required": "Calculation type is required.", + "summary_calculate_type_invalid": "Calculation type is invalid.", + "summary_no_valid_metrics_found": "No valid metrics found.", + "summary_stream_response_not_supported": "Streaming response is not supported.", + "summary_weight_sum_must_be_100": "The total weight of all metrics must be 100.", + + "dataset_collection_id_required": "Dataset ID is required.", + "dataset_collection_update_failed": "Failed to update dataset.", + "dataset_model_not_found": "Model not found.", + "dataset_no_data": "The dataset is empty.", + "dataset_data_id_required": "Dataset item ID is required.", + "data_quality_status_invalid": "The data quality is invalid.", + "dataset_data_user_input_required": "User input is required.", + "dataset_data_expected_output_required": "Expected output is required.", + "dataset_data_actual_output_must_be_string": "The actual output must be a string.", + "dataset_data_context_must_be_array_of_strings": "The context must be an array of strings.", + "dataset_data_retrieval_context_must_be_array_of_strings": "The search context must be an array of strings.", + "dataset_data_enable_quality_eval_required": "Quality evaluation flag must be enabled.", + "dataset_data_evaluation_model_required_for_quality": "The evaluation model is required when the quality evaluation is enabled.", + "dataset_data_metadata_must_be_object": "Metadata must be an object.", + "dataset_data_list_error": "The dataset item list is invalid.", + "quality_assessment_failed": "Quality evaluation failed.", + "data_quality_job_active_cannot_set_high_quality": "Unable to mark as high quality because a quality evaluation task is in progress.", + "dataset_task_not_retryable": "The task cannot be retried.", + "dataset_task_job_not_found": "Task job not found.", + "dataset_task_job_mismatch": "The task job does not belong to this dataset.", + "dataset_task_only_failed_can_delete": "Only failed tasks can be deleted.", + "dataset_task_operation_failed": "Failed to operate the task.", + "dataset_task_delete_failed": "Failed to delete the dataset task.", + "fetch_failed_tasks_error": "Failed to retrieve failed tasks.", + "file_id_required": "File ID is required.", + "file_must_be_csv": "The file must be in CSV format.", + "csv_invalid_structure": "The CSV file structure is invalid.", + "csv_parsing_error": "CSV parsing error occurred.", + "csv_no_data_rows": "The CSV file contains no data rows.", + "count_must_be_greater_than_zero": "The data size must be greater than zero.", + "count_exceeds_available_data": "The requested data size exceeds the available data in the selected knowledge base.", + "selected_datasets_contain_no_data": "The selected dataset is empty.", + "model_name_invalid": "The evaluation model name must be a string.", + "model_name_too_long": "The evaluation model name cannot exceed 100 characters.", + "description_invalid_type": "The description must be a string." } diff --git a/packages/web/i18n/en/file.json b/packages/web/i18n/en/file.json index f63fdc157693..b638f2349e37 100644 --- a/packages/web/i18n/en/file.json +++ b/packages/web/i18n/en/file.json @@ -1,48 +1,48 @@ { - "Action": "Please select the images to be uploaded.", + "Action": "Please select an image to upload.", "All images import failed": "Failed to import all images.", "Dataset_ID_not_found": "The dataset ID does not exist.", - "Failed_to_get_token": "Failed to obtain token.", + "Failed_to_get_token": "Failed to obtain the token.", "Image_ID_copied": "ID copied successfully.", "Image_Preview": "Image preview", - "Image_dataset_requires_VLM_model_to_be_configured": "The image dataset requires the configuration of a visual language model (VLM) to be used. Please first add a model that supports image understanding in the model configuration.", + "Image_dataset_requires_VLM_model_to_be_configured": "To use an image dataset, please go to Model configuration to add a model that supports image recognition first.", "Image_does_not_belong_to_current_team": "The image does not belong to the current team.", "Image_file_does_not_exist": "The image does not exist.", "Loading_image": "Loading image...", - "Loading_image failed": "Failed to load the preview.", + "Loading_image failed": "Failed to load the image for preview.", "Only_support_uploading_one_image": "Only one image can be uploaded.", - "Please select the image to upload": "Please select the images to be uploaded.", - "Please wait for all files to upload": "Please wait for file upload to complete.", + "Please select the image to upload": "Please select an image to upload.", + "Please wait for all files to upload": "Please wait until all files are uploaded.", "bucket_chat": "Chat file", "bucket_file": "Knowledge base file", - "eval_file": "Evaluation Files", + "eval_file": "File", "bucket_image": "Image", "click_to_view_raw_source": "Click to view source", "common.Some images failed to process": "Failed to process some images.", - "common.dataset_data_input_image_support_format": "Supported formats: .jpg, .jpeg, .png, .gif, .webp", + "common.dataset_data_input_image_support_format": "Supported: .jpg, .jpeg, .png, .gif, and .webp", "count.core.dataset.collection.Create Success": "{{count}} images imported successfully.", "delete_image": "Delete image", "file_name": "File name", "file_size": "File size", "image": "Image", - "image_collection": "Collection", + "image_collection": "Image collection", "image_description": "Description", - "image_description_tip": "This field is required.", - "please_upload_image_first": "Please upload the image first.", - "reached_max_file_count": "The number of files has reached the maximum.", - "release_the_mouse_to_upload_the_file": "Release to upload the file", - "select_and_drag_file_tip": "Drag & drop or click to upload file", + "image_description_tip": "", + "please_upload_image_first": "Please upload an image first.", + "reached_max_file_count": "The number of files reaches the maximum.", + "release_the_mouse_to_upload_the_file": "Release to start upload", + "select_and_drag_file_tip": "Drag & drop or click to upload", "select_file_amount_limit": "Up to {{max}} files can be selected.", - "some_file_count_exceeds_limit": "Exceeded {{maxCount}} files, automatically truncated", - "some_file_size_exceeds_limit": "Some files exceed {{maxSize}} and have been filtered.", - "support_file_type": "Supported file type: {{fileType}}", - "support_max_count": "Up to {{maxCount}} files are supported.", - "support_max_size": "The file size cannot exceed {{maxSize}}.", - "template_csv_file_select_tip": "Must be a {{fileType}} file strictly consistent with the template.", - "template_strict_highlight": "Strictly follow the template", - "total_files": "{{selectFiles.length}} files in total", + "some_file_count_exceeds_limit": "The number of selected files exceeds the maximum ({{maxCount}}). The additional files were automatically ignored.", + "some_file_size_exceeds_limit": "Some files were removed because their size exceeds the maximum ({{maxSize}}).", + "support_file_type": "Supported: {{fileType}}", + "support_max_count": "Maximum: {{maxCount}} files,", + "support_max_size": "{{maxSize}} or less per file", + "template_csv_file_select_tip": "Supported: {{fileType}} file strictly consistent with the template", + "template_strict_highlight": "Strictly consistent with the template", + "total_files": "Total files: {{selectFiles.length}}", "upload_error_description": "Only multiple files or one folder can be uploaded at a time.", - "upload_failed": "Error occurred while uploading.", - "upload_file_error": "This field is required.", + "upload_failed": "Error occurred during the upload.", + "upload_file_error": "Please upload an image.", "uploading": "Uploading..." } diff --git a/packages/web/i18n/en/login.json b/packages/web/i18n/en/login.json index 2020d6ec5bd9..185b1012205c 100644 --- a/packages/web/i18n/en/login.json +++ b/packages/web/i18n/en/login.json @@ -1,21 +1,21 @@ { - "Chinese_ip_tip": "Detected that you are using a Mainland China IP. Click to visit the Mainland China version.", + "Chinese_ip_tip": "Your IP address is in Mainland China. Click to use the Chinese Mainland edition.", "Login": "Log in", "agree": "Agree", - "cookies_tip": "This website uses cookies to provide a better service experience. By continuing to use the site, you agree to our Cookie Policy.", + "cookies_tip": "This website uses cookies to improve your experience. By continuing to use this website, you agree to the cookie policy.", "forget_password": "Forgot password?", "login_failed": "Login error occurred.", "login_success": "Login successful.", - "no_remind": "Do not show this again.", - "password_condition": "Password cannot exceed 60 characters long.", - "password_tip": "Password must be at least 8 characters long and contain at least 2 of the following: digits, letters, and special characters.", - "policy_tip": "By using this service, you acknowledge that you have read and agree to the Terms of Use> and Privacy Policy.", - "privacy": "Privacy terms", - "privacy_policy": "Privacy Policy", - "redirect": "Redirect", + "no_remind": "Do not show this again", + "password_condition": "Password cannot exceed 60 characters.", + "password_tip": "Must be at least 8 characters long and contain at least 2 of the following: digits, letters, and special characters.", + "policy_tip": "Your use of this product indicates that you have read and accept the EULA and DPA", + "privacy": "DPA", + "privacy_policy": "DPA", + "redirect": "Go now", "register": "Sign up", - "root_password_placeholder": "The root user password is the value of the environment variable DEFAULT_ROOT_PSW.", - "terms": "Terms of use", - "use_root_login": "Log in as the root user", + "root_password_placeholder": "The root account password is the value of the environment variable DEFAULT_ROOT_PSW.", + "terms": "EULA", + "use_root_login": "Log in as root", "wecom": "WeCom" } diff --git a/packages/web/i18n/en/publish.json b/packages/web/i18n/en/publish.json index b791ba4b7505..786722384f84 100644 --- a/packages/web/i18n/en/publish.json +++ b/packages/web/i18n/en/publish.json @@ -1,31 +1,31 @@ { - "app_key_tips": "These keys already have the current application identifier. For specific usage, please refer to the documentation.", + "app_key_tips": "These keys have already been associated with the current app ID. For details about how to use them, see the documentation.", "basic_info": "Basics", - "config": "Visibility configuration", - "copy_link_hint": "Copy the link to the specified location", + "config": "Visibility", + "copy_link_hint": "Copy the following link to the specified location.", "create_api_key": "Create key", "create_link": "Create link", - "edit_api_key": "Edit key information", + "edit_api_key": "Edit key", "edit_feishu_bot": "Edit Feishu bot", "edit_link": "Edit", - "feishu_api": "Feishu interface", + "feishu_api": "Feishu API", "feishu_bot": "Feishu bot", "feishu_bot_desc": "Connect to Feishu bot through API", "key_alias": "Alias of the key. For display only.", "key_tips": "You can use an API key to access certain interfaces. (Accessing apps requires in-app API keys.)", - "link_name": "Name of the shared link", + "link_name": "Name of the sharing link", "new_feishu_bot": "Add Feishu bot", - "official_account.create_modal_title": "Create WeChat Official Account Access", - "official_account.desc": "Directly connect to the WeChat Official Account via API", + "official_account.create_modal_title": "Create WeChat official account connection", + "official_account.desc": "Connect to WeChat official account via API", "official_account.edit_modal_title": "Edit WeChat official account connection", "official_account.name": "WeChat official account connection", "official_account.params": "WeChat official account parameter", - "private_config": "Visibility configuration", + "private_config": "Visibility", "publish_name": "Name", "qpm_is_empty": "QPM is required.", - "qpm_tips": "How many questions can be asked per minute per IP?", + "qpm_tips": "The maximum number of questions allowed per minute from an IP address", "request_address": "Request URL", - "show_node": "realtime operating status", + "show_node": "Realtime running status", "show_share_link_modal_title": "Use now", "token_auth": "Authentication", "token_auth_tips": "Authentication server address", @@ -35,11 +35,11 @@ "wecom.bot_desc": "Connect to WeCom bot through API", "wecom.create_modal_title": "Create WeCom bot", "wecom.edit_modal_title": "Edit WeCom bot", - "wecom.title": "Publish app to WeCom bot", + "wecom.title": "Publish app via WeCom bot", "dingtalk.bot": "DingTalk bot", "dingtalk.bot_desc": "Connect to DingTalk bot through API", "dingtalk.create_modal_title": "Create DingTalk bot", "dingtalk.edit_modal_title": "Edit DingTalk bot", - "dingtalk.title": "Publish app to DingTalk bot", + "dingtalk.title": "Publish app via DingTalk bot", "dingtalk.api": "DingTalk API" } diff --git a/packages/web/i18n/en/user.json b/packages/web/i18n/en/user.json index 2fa238d41b93..8589f8fbf031 100644 --- a/packages/web/i18n/en/user.json +++ b/packages/web/i18n/en/user.json @@ -5,112 +5,112 @@ "bill.conversion": "Redeem", "bill.convert_error": "Operation failed.", "bill.convert_success": "Redeemed successfully.", - "bill.current_token_price": "Current point price", - "bill.not_need_invoice": "Balance payment, unable to issue an invoice", + "bill.current_token_price": "Current price for credit", + "bill.not_need_invoice": "Unable to issue an invoice because the payment was deducted from the balance.", "bill.price": "Unit price", "bill.renew_plan": "Renew plan", "bill.standard_valid_tip": "Plan usage rules: Higher-level plans are used first. Unused lower-level plans will be applied later.", - "bill.token_expire_1year": "Points are valid for 1 year.", - "bill.tokens": "Points", - "bill.use_balance": "Use balance", - "bill.use_balance_hint": "Due to a system upgrade, renewal with auto reduction from balance has been disabled, and balance top-up is no longer available. Your balance can still be used to purchase points.", + "bill.token_expire_1year": "Credits are valid for 1 year.", + "bill.tokens": "Credits", + "bill.use_balance": "Pay by balance", + "bill.use_balance_hint": "Due to a system upgrade, renewal with auto reduction from balance has been disabled, and balance top-up is no longer available. Your balance can still be used to purchase credits.", "bill.valid_time": "Valid since", "bill.you_can_convert": "You can redeem", "bill.yuan": "CNY", - "delete.admin_failed": "Failed to delete admin.", + "delete.admin_failed": "Failed to delete the admin.", "delete.admin_success": "Admin deleted successfully.", "delete.failed": "Operation failed.", "delete.success": "Deleted successfully.", "has_chosen": "Selected", - "login.Dingtalk": "DingTalk Login", + "login.Dingtalk": "DingTalk login", "login.error": "Login error occurred.", - "login.password_condition": "Password cannot exceed 60 characters long.", + "login.password_condition": "Password cannot exceed 60 characters.", "login.success": "Login successful.", "manage_team": "Management team", "name": "Name", "new_password": "New password", - "notification.remind_owner_bind": "Remind the creator to specify an account to receive notifications.", + "notification.remind_owner_bind": "Please remind the creator to specify an account to receive notifications.", "operations": "Operation", "owner": "Owner", "password.code_required": "Verification code is required.", "password.code_send_error": "Error occurred while sending the verification code.", "password.code_sended": "Verification code sent successfully.", "password.confirm": "Confirm password", - "password.email_phone_error": "Email or mobile number is invalid.", - "password.email_phone_void": "Email and mobile number are required.", + "password.email_phone_error": "Email address or mobile number is invalid.", + "password.email_phone_void": "Email address and mobile number are required.", "password.not_match": "Passwords do not match.", "password.password_condition": "Password must contain 4-20 characters.", - "password.password_required": "The password is required.", - "password.retrieve": "Retrieve Password", - "password.retrieved": "Password has been recovered", - "password.retrieved_account": "Recover {{account}} account", + "password.password_required": "Password is required.", + "password.retrieve": "Reset password", + "password.retrieved": "Password reset successfully.", + "password.retrieved_account": "Retrieve account ({{account}})", "password.to_login": "Log in", - "password.verification_code": "Code", - "permission.Add": "Add permissions", - "permission.Manage": "Administrator", - "permission.Manage tip": "Team administrator, with full permissions", + "password.verification_code": "Verification code", + "permission.Add": "Add permission", + "permission.Manage": "Admin", + "permission.Manage tip": "The team administrator has full permissions.", "permission.Read": "Read-only", - "permission.Read desc": "Members can only read related resources and cannot create new ones.", - "permission.Write": "Write", - "permission.Write tip": "In addition to readable resources, new resources can also be created.", - "permission.only_collaborators": "Collaborators only", - "permission.team_read": "Team accessible", - "permission.team_write": "Edit team", - "permission_add_tip": "After adding, you can select permissions for it.", - "permission_des.manage": "Can create resources, invite, and remove members", - "permission_des.read": "Members can only read related resources and cannot create new ones.", - "permission_des.write": "In addition to readable resources, new resources can also be created.", + "permission.Read desc": "Members can only read related resources but cannot create new ones.", + "permission.Write": "Read and write", + "permission.Write tip": "Members can read and create resources.", + "permission.only_collaborators": "Accessible to collaborators only", + "permission.team_read": "Accessible to team", + "permission.team_write": "Editable by team", + "permission_add_tip": "After the member is added, you can grant permissions to it.", + "permission_des.manage": "Admins can create resources and invite or delete members.", + "permission_des.read": "Members can only read related resources but cannot create new ones.", + "permission_des.write": "Members can read and create resources.", "permissions": "Permission", - "personal_information": "Personal info", + "personal_information": "Profile", "personalization": "Personalization", "promotion_records": "Promotion Records", - "register.confirm": "Confirm Registration", + "register.confirm": "Confirm", "register.register_account": "Register {{account}} account", "register.success": "Registered successfully.", - "register.to_login": "Have an account? Sign In", - "search_group_org_user": "Search member or group name", - "search_user": "Search username", - "sso_auth_failed": "SSO authentication failed", - "synchronization.button": "Sync Now", - "synchronization.placeholder": "Please enter the synchronization label", - "synchronization.title": "Fill in the label sync link, click the sync button to synchronize.", - "team.Add manager": "Add account", - "team.Confirm Invite": "Confirm Invitation", - "team.Create Team": "Create a new team", - "team.Invite Member Failed Tip": "Inviting members encountered an exception", - "team.Invite Member Result Tip": "Invitation Result Notification", - "team.Invite Member Success Tip": "Invite members to complete\nSuccess: {{success}} people\nUsername invalid: {{inValid}}\nAlready in the team: {{inTeam}}", + "register.to_login": "Have an account? Sign in", + "search_group_org_user": "Member, department, group name", + "search_user": "Username", + "sso_auth_failed": "SSO authentication failed.", + "synchronization.button": "Sync now", + "synchronization.placeholder": "Tag to be synced", + "synchronization.title": "To sync tags, enter the tag sync link and click Sync now.", + "team.Add manager": "Add admin", + "team.Confirm Invite": "Confirm", + "team.Create Team": "Create team", + "team.Invite Member Failed Tip": "Error occurred while inviting members.", + "team.Invite Member Result Tip": "Message", + "team.Invite Member Success Tip": "Invitation completed.\nMembers joined successfully: {{success}}\nInvalid usernames: {{inValid}}\nMembers already in the team: {{inTeam}}", "team.Set Name": "Name the team", - "team.Team Name": "Community", - "team.Update Team": "Update team information", - "team.add_collaborator": "Add Collaborator", - "team.add_permission": "Add permissions", + "team.Team Name": "Team name", + "team.Update Team": "Edit team info", + "team.add_collaborator": "Add collaborator", + "team.add_permission": "Add permission", "team.add_writer": "Add writable members", - "team.avatar_and_name": "Tool image & name", - "team.belong_to_group": "Group:", - "team.group.avatar": "Group avatar", + "team.avatar_and_name": "Icon image & name", + "team.belong_to_group": "Group", + "team.group.avatar": "Group profile image", "team.group.create": "Create group", - "team.group.create_failed": "Failed to create the user group.", + "team.group.create_failed": "Failed to create the group.", "team.group.default_group": "Default group", "team.group.delete_confirm": "Are you sure you want to delete the group?", - "team.group.edit": "Edit Group", + "team.group.edit": "Edit group", "team.group.edit_info": "Edit", "team.group.group": "Group", - "team.group.keep_admin": "Admin", - "team.group.manage_member": "Manage member", + "team.group.keep_admin": "Retain admin permissions", + "team.group.manage_member": "Manage members", "team.group.manage_tip": "Manage members, create groups, manage all groups, and assign permissions to groups and members.", "team.group.members": "Member", - "team.group.name": "Name", - "team.group.permission_tip": "Members with individually configured permissions will follow their personal permission settings and will no longer be affected by group permissions.\nIf a member is in multiple permission groups, the member's permissions will be the union of all permissions.", - "team.group.role.admin": "Administrator", + "team.group.name": "Group name", + "team.group.permission_tip": "Members with separately configured permissions will not be affected by group permissions.\nIf a member is added to multiple permission groups, their permissions are combined.", + "team.group.role.admin": "Admin", "team.group.role.member": "Member", "team.group.role.owner": "Owner", - "team.group.set_as_admin": "Set as administrator", - "team.group.toast.can_not_delete_owner": "Cannot delete owner, please transfer ownership first.", + "team.group.set_as_admin": "Set as admin", + "team.group.toast.can_not_delete_owner": "The owner cannot be deleted. Please transfer ownership first.", "team.group.transfer_owner": "Transfer ownership", - "team.manage_collaborators": "Manage Collaborators", - "team.no_collaborators": "No collaborators", + "team.manage_collaborators": "Manage collaborators", + "team.no_collaborators": "No collaborators available.", "team.org.org": "Department", - "team.write_role_member": "Writable permission", - "team.collaborator.added": "Added" + "team.write_role_member": "Write permission", + "team.collaborator.added": "Added successfully." } diff --git a/packages/web/i18n/en/workflow.json b/packages/web/i18n/en/workflow.json index e26cf3393bd1..d007e6f37d36 100644 --- a/packages/web/i18n/en/workflow.json +++ b/packages/web/i18n/en/workflow.json @@ -1,225 +1,225 @@ { - "Array_element": "Array elements", - "Array_element_index": "subscript", - "Click": "Click", + "Array_element": "Array element", + "Array_element_index": "Subscript", + "Click": "Click to", "Code": "Code", - "Confirm_sync_node": "The configuration will be updated to the latest node settings, and any fields not present in the template will be removed (including all custom fields).\nIf the field is relatively complex, it is recommended that you first make a copy of the node and then update the original node to facilitate parameter copying.", - "Drag": "Drag and drop", - "Node.Open_Node_Course": "View Node Tutorial", - "Node_variables": "Node variables", - "Quote_prompt_setting": "Reference prompt configuration", - "Variable.Variable type": "Type", - "Variable_name": "Variable Name", + "Confirm_sync_node": "Nodes will be updated to the latest configuration. Fields that do not exist in the template (including all custom fields) will be deleted.\nIf the fields are complex, it is recommended to copy the node before updating it.", + "Drag": "Drag to", + "Node.Open_Node_Course": "Guide", + "Node_variables": "Node variable", + "Quote_prompt_setting": "Configure prompt", + "Variable.Variable type": "Variable type", + "Variable_name": "Variable name", "add_new_input": "Add input", - "add_new_output": "Add input", - "append_application_reply_to_history_as_new_context": "Concatenate the app's reply content to the history record and return it as the new context.", - "application_call": "App invocation", - "assigned_reply": "Specified reply", - "auth_tmb_id": "User Authentication", - "auth_tmb_id_tip": "When enabled, when the application is published externally, it will also filter the knowledge base based on whether the user has access to it.\nIf not enabled, it will directly retrieve from the configured knowledge base without permission filtering.", - "auto_align": "Auto-align", - "can_not_loop": "This node does not support loop nesting", + "add_new_output": "Add output", + "append_application_reply_to_history_as_new_context": "Combine the response from the app with the chat history to output a new context.", + "application_call": "App call", + "assigned_reply": "Specified answer", + "auth_tmb_id": "User authentication", + "auth_tmb_id_tip": "If enabled, a knowledge base will not be used if the user does not have access permissions to it when the app is published.\nIf disabled, the system will search from the selected knowledge bases without permission-based filtering.", + "auto_align": "Auto align", + "can_not_loop": "The node does not support circular nesting.", "choose_another_application_to_call": "Select another app to call", - "classification_result": "Classification results", - "click_to_change_reference": "Click to switch input mode", - "click_to_change_value": "Click to toggle variable reference mode", - "code.Reset template": "Restored", - "code.Reset template confirm": "Confirm restore code template? All inputs and outputs will be reset to template values. Please save your current code.", - "code.Switch language confirm": "Switching languages will reset the code, do you want to continue?", - "code_execution": "Code Execution", - "collection_metadata_filter": "Filter collection metadata", - "complete_extraction_result": "Full extraction result", - "complete_extraction_result_description": "A JSON string. Example:{\"name\":\"YY\",\"Time\":\"2023/7/2 18:00\"}", + "classification_result": "Classification result", + "click_to_change_reference": "Click to switch to input mode", + "click_to_change_value": "Click to switch to variable reference mode", + "code.Reset template": "Restore template", + "code.Reset template confirm": "Are you sure you want to restore the code template? All inputs and outputs will be reset to the default values. Please save the current code first.", + "code.Switch language confirm": "Changing the language will reset the code. Would you like to proceed?", + "code_execution": "Code running", + "collection_metadata_filter": "Collection metadata filtering", + "complete_extraction_result": "Complete extraction result", + "complete_extraction_result_description": "A JSON string. Example: {\"name\":\"YY\",\"Time\":\"2023/7/2 18:00\"}", "concatenation_result": "Splicing result", - "concatenation_text": "Splicing text", + "concatenation_text": "Text for splicing", "condition_checker": "Judger", - "confirm_delete_field_tip": "Are you sure that you want to delete this field?", - "contains": "Contains", + "confirm_delete_field_tip": "Are you sure you want to delete this field?", + "contains": "contains", "content_to_retrieve": "Content to be searched", "content_to_search": "Content to be searched", "contextMenu.addComment": "Add comment", "context_menu.add_comment": "Add comment", - "create_link_error": "Link creation failed", + "create_link_error": "Error occurred while creating the link.", "custom_feedback": "Custom feedback", "custom_input": "Custom input", "dataset_quote_role": "Role", - "dataset_quote_role_system_option_desc": "Prefer conversation continuity (recommended)", - "dataset_quote_role_tip": "System: Places knowledge base references in system messages to maintain conversation continuity. Constraints may be weaker, so additional tuning could be needed.\nUser: Places knowledge base references in user messages and requires specifying the {{question}} variable. Continuity may be slightly reduced, but constraints are usually enforced more effectively.", - "dataset_quote_role_user_option_desc": "Prefer strict constraints", - "dynamic_input_description": "Receive output values from frontend nodes as variables, which are available for Laf request parameters.", + "dataset_quote_role_system_option_desc": "Coherence prioritized (recommended)", + "dataset_quote_role_tip": "System: Knowledge base references will be included in system messages for coherence, but constraints may be less effective. Additional debugging may be needed.\nUser: Knowledge base references will be included in user messages, and the position of the {{question}} variable must be specified. This may slightly affect coherence, but constraints can be enforced more effectively.", + "dataset_quote_role_user_option_desc": "Constraints prioritized", + "dynamic_input_description": "Output values from the upstream nodes are received as variables, which can be used in LAF request parameters.", "edit_input": "Edit input", "edit_output": "Edit output", - "end_with": "End with", + "end_with": "ends with", "enter_comment": "Comment", "error_catch": "Error capture", - "error_info_returns_empty_on_success": "Returns error details if the code fails. Returns empty if it succeeds.", + "error_info_returns_empty_on_success": "Error message returned when code execution error occurs. If the code is executed successfully, a null value is returned.", "error_text": "Error details", "execute_a_simple_script_code_usually_for_complex_data_processing": "Run a short script, typically for complex data processing.", - "execute_different_branches_based_on_conditions": "Execute different branches based on conditions.", + "execute_different_branches_based_on_conditions": "Execute a branch based on the specified conditions.", "execution_error": "Running error", "extraction_requirements_description": "Extraction requirement", - "extraction_requirements_description_detail": "Provide relevant context or instructions to guide the AI in completing the task accurately. \\nYou can use global variables in this field.", - "extraction_requirements_placeholder": "Example: 1. Current time: {{cTime}}. You are a lab booking assistant tasked with extracting reservation info from text.\n2. You are a Google search assistant tasked with extracting suitable search terms from text.", - "feedback_text": "Feedback text", + "extraction_requirements_description_detail": "Provide relevant context or prompts to guide extraction. \\nGlobal variables are supported.", + "extraction_requirements_placeholder": "Example: 1. Current time: {{cTime}} You are a lab booking assistant and need to extract lab booking information from the text.\n2. You are a Google search assistant and need to extract suitable keywords for Google searches from the text.", + "feedback_text": "Feedback", "field_description": "Field description", - "field_description_placeholder": "Describe the function of this input field. For tool parameters, this description affects the output quality of the model.", + "field_description_placeholder": "Describe the function of the input field. If the field is used as a tool call parameter, the description will affect the output quality of the model.", "field_name_already_exists": "The field name already exists.", "field_required": "Required", - "field_used_as_tool_input": "Used as tool parameter.", - "filter_description": "Currently supports tag and creation-time filtering. Fill in the following format: { ... }\n{\n \"tags\": {\n \"$and\": [\"Tag 1\",\"Tag 2\"],\n \"$or\": [\"When $and tags exist, the $and condition applies and the $or condition is ignored\"]\n },\n \"createTime\": {\n \"$gte\": \"Use the format YYYY-MM-DD HH:mm. The collection's creation time must be later than this time\"\n \"$lte\": \"Use the format YYYY-MM-DD HH:mm. The collection's creation time must be earlier than this time and can be used together with $gte\"\n }\n}", + "field_used_as_tool_input": "Used as a tool call parameter", + "filter_description": "Knowledge bases can be filtered based on tags and the creation time. Format: \n{\n \"tags\": {\n \"$and\": [\"Tag 1\",\"Tag 2\"],\n \"$or\": [\"When the $and tags exist, the AND condition applies and the OR condition is ignored.\"]\n },\n \"createTime\": {\n \"$gte\": \"Format: YYYY-MM-DD HH:mm. The collection's creation time must be later than this time.\"\n \"$lte\": \"Format: YYYY-MM-DD HH:mm. The collection's creation time must be earlier than this time. $lte and $gte can be used together.\"\n }\n}", "find_tip": "Find node (Ctrl + F)", "find_tip_mac": "Find node (⌘ + F)", "foldAll": "Collapse all", - "form_input_result": "Full input", + "form_input_result": "Complete user input", "form_input_result_tip": "An object containing the full result", - "full_field_extraction": "Extract full field", - "full_field_extraction_description": "Returns true if all fields are filled (either from model extraction or default values)", - "full_response_data": "Complete response data", - "greater_than": "Greater than", - "greater_than_or_equal_to": "Greater than or equal to", - "http_body_placeholder": "Syntax similar to APIFox. Use / to activate variable selection. String variables require double quotes. Other types do not. Ensure valid JSON format", - "http_extract_output": "Extract output field", - "http_extract_output_description": "Extract specific fields from the response using JSONPath.", - "http_raw_response_description": "Raw HTTP response. Only string or JSON type data is accepted.", + "full_field_extraction": "Complete field extraction", + "full_field_extraction_description": "Returns true if all fields are filled in (either from model extraction or default values)", + "full_response_data": "Complete response", + "greater_than": "is greater than", + "greater_than_or_equal_to": "is greater than or equal to", + "http_body_placeholder": "Use a syntax similar to APIFox. Enter a slash (/) to select a variable. Only string variables must be enclosed in double quotation marks. Ensure the string is in JSON format.", + "http_extract_output": "Output field extraction", + "http_extract_output_description": "Extract specific fields from the response using the JSONPath syntax.", + "http_raw_response_description": "Raw HTTP response. The response data must be a string or in JSON format.", "http_request": "HTTP request", "ifelse.Input value": "Input", "ifelse.Select value": "Select", "input_description": "Field description", "input_type_multiple_select": "Checkbox", - "input_variable_list": "Enter / to trigger variables.", - "intro_assigned_reply": "This module can reply with a specified message, often used for guidance or prompts. Non-string content is automatically converted to a string.", + "input_variable_list": "Enter a slash (/) to select a variable.", + "intro_assigned_reply": "This module supports replies with a specified message, often used as guidance or prompts. Non-string content is automatically converted to a string.", "intro_custom_feedback": "When triggered, this module adds feedback to the current conversation, useful for automatically tracking response quality.", - "intro_custom_plugin_output": "Customize external output. When using a plugin, only the configured output is exposed.", - "intro_http_request": "Send HTTP requests to perform complex actions such as online searches or database queries.", - "intro_knowledge_base_search_merge": "Merge search results from multiple knowledge bases and sort them using the RRF method.", - "intro_laf_function_call": "Invoke cloud functions under the Laf account.", + "intro_custom_plugin_output": "Customize external output. When a plugin is used, only the custom output is exposed.", + "intro_http_request": "Send an HTTP request to perform a more complex action (such as online search or database query).", + "intro_knowledge_base_search_merge": "Merge search results from multiple knowledge bases and sort them using the RRF method to provide output.", + "intro_laf_function_call": "Call cloud functions under the LAF account.", "intro_loop": "Input an array and iterate over each element to execute the workflow.", - "intro_plugin_input": "Configure required plugin inputs and use them to run the plugin.", - "intro_question_classification": "Determine the question type based on the user’s history and the current question. You can define multiple types, for example:\nType 1: Greeting\nType 2: Product usage\nType 3: Product purchase\nType 4: Other.", - "intro_question_optimization": "Use the question optimization feature to increase the accuracy of knowledge base searches during continuous chats. AI generates new search terms based on context for better results. This feature is built into the knowledge base search module and can also be used for single searches.", - "intro_text_concatenation": "Process fixed or input text and output it. Non-string data will be converted to strings automatically.", + "intro_plugin_input": "Configure inputs required for running a plugin.", + "intro_question_classification": "Identify the question type based on the current question and the user's chat history. You can add multiple question types. Example:\nType 1: Greeting\nType 2: Product usage\nType 3: Product purchase\nType 4: Other", + "intro_question_optimization": "Use the question optimization feature to increase the accuracy of knowledge base searches during continuous chats. The model will generate one or multiple new keywords based on the context to optimize the search results. This feature is built into the knowledge base search module and can also be used for a single search.", + "intro_text_concatenation": "Process fixed or input text and then provide an output. Non-string data will be automatically converted into strings.", "intro_text_content_extraction": "Extract specific data from text, such as SQL queries, search keywords, or code.", - "intro_tool_call_termination": "This module requires tool invocation. When executed, it forcibly ends the current tool call and does not use AI to answer the tool’s output.", - "intro_tool_params_config": "This module works with tool calls. You can customize parameters and pass them to downstream nodes.", + "intro_tool_call_termination": "This module works with tool calling. When this module is executed, the tool call is terminated, and no replies are provided based on the tool call result.", + "intro_tool_params_config": "This module works with tool calling. Customize tool call parameters and pass them to downstream nodes.", "is_empty": "is null", - "is_equal_to": "is equal to", + "is_equal_to": "is", "is_not_empty": "is not null", "is_not_equal": "is not", "is_tool_output_label": "As tool response", - "judgment_result": "Judger result", + "judgment_result": "Result", "knowledge_base_reference": "Knowledge base reference", - "knowledge_base_search_merge": "Merge knowledge base search results", - "laf_function_call_test": "Call Laf function (test)", - "length_equal_to": "Length equals", - "length_greater_than": "Length greater than", - "length_greater_than_or_equal_to": "Length greater than or equal to", - "length_less_than": "Length less than", - "length_less_than_or_equal_to": "Length less than or equal to", - "length_not_equal_to": "Length not equal to", - "less_than": "Less than", - "less_than_or_equal_to": "Less than or equal to", + "knowledge_base_search_merge": "Reference merging", + "laf_function_call_test": "LAF function call (Beta)", + "length_equal_to": "whose length is", + "length_greater_than": "whose length is longer than", + "length_greater_than_or_equal_to": "whose length is not shorter than", + "length_less_than": "whose length is shorter than", + "length_less_than_or_equal_to": "whose length is not longer than", + "length_not_equal_to": "whose length is not", + "less_than": "is smaller than", + "less_than_or_equal_to": "is smaller than or equal to", "loop": "Bulk execution", "loop_body": "Loop body", - "loop_end": "End", + "loop_end": "Finish", "loop_input_array": "Array", "loop_result": "Array execution result", "loop_start": "Start", - "max_dialog_rounds": "Max chat record", + "max_dialog_rounds": "Maximum number of chats remembered", "max_tokens": "Max tokens", - "mouse_priority": "Prefer mouse\nLeft-click and hold to drag the canvas.\nHold Shift + left-click to select multiple items.", + "mouse_priority": "Mouse operations\n- Click and hold to drag the canvas.\n- Press Shift and click to select multiple objects.", "new_context": "New context", "next": "Next", "no_match_node": "No result", - "no_node_found": "No node found", + "no_node_found": "No node found.", "not_contains": "does not contain", "only_the_reference_type_is_supported": "Reference type only", - "optional_value_type": "Optional data type", - "optional_value_type_tip": "You can specify one or more data types. When dynamically adding fields, users can only select the configured types.", - "pan_priority": "Touchpad first\n- Click to batch select\n- Move the canvas with two fingers", - "pass_returned_object_as_output_to_next_nodes": "Pass the object returned in the code as output to the next nodes. The variable name needs to correspond to the return key.", - "please_enter_node_name": "Enter the node name", - "plugin.Instruction_Tip": "You can configure an instruction to explain the purpose of the plugin. This instruction will be displayed each time the plugin is used. Supports standard Markdown syntax.", - "plugin.Instructions": "Instructions", + "optional_value_type": "Available data types", + "optional_value_type_tip": "You can specify one or multiple data types. When users add fields, they can only choose from these data types.", + "pan_priority": "Touchpad operations\n- Tap to select multiple objects.\n- Tap with two fingers to drag the canvas.", + "pass_returned_object_as_output_to_next_nodes": "Use the return value as the output and pass it to the next node. The variable name must match the key in the return value.", + "please_enter_node_name": "Node name", + "plugin.Instruction_Tip": "You can add a description to explain the purpose of the plugin. The description will be displayed each time before the plugin is used. Standard Markdown syntax is supported.", + "plugin.Instructions": "Guide", "plugin.global_file_input": "File links (deprecated)", "plugin_file_abandon_tip": "Plugin global file upload has been deprecated, please adjust it as soon as possible. \nRelated functions can be achieved through plug-in input and adding image type input.", - "plugin_input": "Plugin Input", - "plugin_output_tool": "When the plug-in is executed as a tool, whether this field responds as a result of the tool", + "plugin_input": "Plugin input", + "plugin_output_tool": "Whether the field is used as the output when the plugin runs as a tool.", "previous": "Previous", - "question_classification": "Classify", - "question_optimization": "Query extension", - "quote_content_placeholder": "The structure of the reference content can be customized to better suit different scenarios. \nSome variables can be used for template configuration\n\n{{q}} - main content\n\n{{a}} - auxiliary data\n\n{{source}} - source name\n\n{{sourceId}} - source ID\n\n{{index}} - nth reference", - "quote_content_tip": "The structure of the reference content can be customized to better suit different scenarios. Some variables can be used for template configuration:\n\n{{id}} - the unique id of the reference data\n{{q}} - main content\n{{a}} - auxiliary data\n{{source}} - source name\n{{sourceId}} - source ID\n{{index}} - nth reference\n\nThey are all optional and the following are the default values:\n\n{{default}}", - "quote_num": "Dataset", - "quote_prompt_tip": "You can use {{quote}} to insert a quote content template and {{question}} to insert a question (Role=user).\n\nThe following are the default values:\n\n{{default}}", - "quote_role_system_tip": "Please note that the {{question}} variable is removed from the \"Quote Template Prompt Words\"", - "quote_role_user_tip": "Please pay attention to adding the {{question}} variable in the \"Quote Template Prompt Word\"", - "raw_response": "Raw Response", - "reasoning_text": "Thinking text", + "question_classification": "Question Classifier", + "question_optimization": "Question optimization", + "quote_content_placeholder": "Customize the structure of the referenced content to fit different scenarios. Use variables to configure the template.\n{{q}} - Main content\n{{a}} - Extra data\n{{source}} - Source name\n{{sourceId}} - Source ID\n{{index}} - Sequence number of reference", + "quote_content_tip": "Customize the structure of the referenced content to fit different scenarios. Use variables to configure the template.\n{{id}} - Unique reference ID\n{{q}} - Main content\n{{a}} - Extra data\n{{source}} - Source name\n{{sourceId}} - Source ID\n{{index}} - Sequence number of reference\nThese variables are all optional. The default values are as follows: {{default}}\n{{default}}", + "quote_num": "Reference", + "quote_prompt_tip": "Use {{quote}} to insert a reference template, and {{question}} to insert a question (Role = User).\nDefault values: \n{{default}}", + "quote_role_system_tip": "Please remove {{question}} from the prompt.", + "quote_role_user_tip": "Please add {{question}} to the prompt.", + "raw_response": "Original response", + "reasoning_text": "Reasoning process", "regex": "Regex", - "reply_text": "Reply Text", - "request_error": "request_error", - "response.Code log": "Code Log", - "response.Custom inputs": "Custom Inputs", - "response.Custom outputs": "Custom Outputs", - "response.Error": "Error", - "response.Read file result": "Read File Result", - "response.read files": "Read Files", - "select_an_application": "Select an Application", - "select_another_application_to_call": "You can choose another application to call", - "select_default_option": "Select the default value", - "special_array_format": "Special array format, returns an empty array when the search result is empty.", - "start_with": "Starts With", - "support_code_language": "Support import list: pandas,numpy", - "target_fields_description": "A target field consists of 'description' and 'key'. Multiple target fields can be extracted.", - "template.agent": "Agent", - "template.agent_intro": "Automatically select one or more functional blocks for calling through the AI model, or call plugins.", - "template.ai_chat": "AI Chat", - "template.ai_chat_intro": "AI Large Model Chat", - "template.dataset_search": "Dataset Search", - "template.dataset_search_intro": "调用语义检索、全文检索、数据库检索能力,从“知识库”中查找可能与问题相关的参考内容", - "template.forbid_stream": "Forbid stream mode", - "template.forbid_stream_desc": "Forces the output mode of nested application streams to be disabled", + "reply_text": "Reply text", + "request_error": "Request error", + "response.Code log": "Log", + "response.Custom inputs": "Custom input", + "response.Custom outputs": "Custom output", + "response.Error": "Error details", + "response.Read file result": "Parsing result preview", + "response.read files": "Parsed document", + "select_an_application": "Select app", + "select_another_application_to_call": "You can select another app to call.", + "select_default_option": "Select", + "special_array_format": "Special array format. An empty array is returned when no results are found.", + "start_with": "starts with", + "support_code_language": "Libraries that can be imported in the code: pandas, numpy", + "target_fields_description": "A target field consists of the description and key. Multiple target fields can be extracted.", + "template.agent": "Tool call", + "template.agent_intro": "The model decides which tool to call.", + "template.ai_chat": "AI chat", + "template.ai_chat_intro": "Chat with an AI model.", + "template.dataset_search": "Knowledge base search", + "template.dataset_search_intro": "Use semantic, full-text, and database search capabilities to search for reference materials related to the questions from the selected knowledge bases.", + "template.forbid_stream": "Disable output in streaming mode", + "template.forbid_stream_desc": "Force nested apps to run in non-streaming mode.", "template.plugin_output": "Plugin output", - "template.plugin_start": "Plugin start", + "template.plugin_start": "Plugin input", "template.system_config": "System", - "template.workflow_start": "Start", - "text_concatenation": "Text Editor", - "text_content_extraction": "Text Extract", - "text_to_extract": "Text to Extract", - "these_variables_will_be_input_parameters_for_code_execution": "These variables will be input parameters for code execution", - "to_add_node": "to add", - "to_connect_node": "to connect", - "tool.tool_result": "Tool operation results", - "tool_active_config": "Tool active", - "tool_active_config_type": "Tool activation: {{type}}", - "tool_call_termination": "Stop ToolCall", - "tool_custom_field": "Custom Tool", - "tool_field": " Tool Field Parameter Configuration", - "tool_input": "Tool Input", + "template.workflow_start": "Process startup", + "text_concatenation": "Text splicing", + "text_content_extraction": "Text extraction", + "text_to_extract": "Text for extraction", + "these_variables_will_be_input_parameters_for_code_execution": "These variables will be used as input parameters for code execution.", + "to_add_node": "add node", + "to_connect_node": "connect to node", + "tool.tool_result": "Tool running result", + "tool_active_config": "Tool activation", + "tool_active_config_type": "Tool activated: {{type}}", + "tool_call_termination": "Tool call termination", + "tool_custom_field": "Custom tool variable", + "tool_field": "Configure tool parameter", + "tool_input": "Tool parameter", "tool_params.enum_placeholder": "apple \npeach \nwatermelon", - "tool_params.enum_values": "Enum values", - "tool_params.enum_values_tip": "List the possible values for this field, one per line", - "tool_params.params_description": "Description", - "tool_params.params_description_placeholder": "Name/Age/SQL statement..", - "tool_params.params_name": "Name", + "tool_params.enum_values": "Enumeration values (optional)", + "tool_params.enum_values_tip": "Valid values for the field. One value per line.", + "tool_params.params_description": "Parameter description", + "tool_params.params_description_placeholder": "Name, age, SQL statement, etc.", + "tool_params.params_name": "Parameter name", "tool_params.params_name_placeholder": "name/age/sql", - "tool_params.tool_params_result": "Parameter configuration results", - "tool_raw_response_description": "The original response of the tool", - "trigger_after_application_completion": "Will be triggered after the application is fully completed", + "tool_params.tool_params_result": "Parameter configuration result", + "tool_raw_response_description": "Original response from tool", + "trigger_after_application_completion": "It will be triggered after the app is fully executed.", "unFoldAll": "Expand all", - "update_link_error": "Error updating link", - "update_specified_node_output_or_global_variable": "Can update the output value of a specified node or update global variables", + "update_link_error": "Error occurred while updating the link.", + "update_specified_node_output_or_global_variable": "Update output for a specified node or update global variables.", "use_user_id": "User ID", - "user_form_input_config": "Form configuration", - "user_form_input_description": "describe", - "user_form_input_name": "Name", - "user_question": "User Question", - "user_question_tool_desc": "User input questions (questions need to be improved)", + "user_form_input_config": "Template settings", + "user_form_input_description": "Description", + "user_form_input_name": "Topic", + "user_question": "Question", + "user_question_tool_desc": "Question (optimization required)", "variable_description": "Variable description", - "variable_picker_tips": "Type node name or variable name to search", - "variable_update": "Variable Update", - "workflow.My edit": "My Edit", - "workflow.Switch_success": "Switch Successful", - "workflow.Team cloud": "Team Cloud", - "workflow.exit_tips": "Your changes have not been saved. 'Exit directly' will not save your edits." -} + "variable_picker_tips": "Enter a node name or variable name.", + "variable_update": "Update variable", + "workflow.My edit": "My edits", + "workflow.Switch_success": "Switched successfully.", + "workflow.Team cloud": "Historical versions", + "workflow.exit_tips": "Your changes are not saved and will be discarded if you exit directly." +} \ No newline at end of file diff --git a/packages/web/i18n/i18next.d.ts b/packages/web/i18n/i18next.d.ts index d0977cf51791..ded2595f4cb0 100644 --- a/packages/web/i18n/i18next.d.ts +++ b/packages/web/i18n/i18next.d.ts @@ -21,6 +21,8 @@ import type chat from './zh-CN/chat.json'; import type login from './zh-CN/login.json'; import type account_model from './zh-CN/account_model.json'; import type dashboard_mcp from './zh-CN/dashboard_mcp.json'; +import type database_client from './zh-CN/database_client.json'; +import type admin from './zh-CN/admin.json'; import type { I18N_NAMESPACES } from './constants'; export interface I18nNamespaces { @@ -47,6 +49,8 @@ export interface I18nNamespaces { account_model: typeof account_model; dashboard_mcp: typeof dashboard_mcp; dashboard_evaluation: typeof dashboard_evaluation; + database_client: typeof database_client; + admin: typeof admin; } export type I18nNsType = (keyof I18nNamespaces)[]; diff --git a/packages/web/i18n/zh-CN/account_bill.json b/packages/web/i18n/zh-CN/account_bill.json index f067e2ae3686..f9648dd6e217 100644 --- a/packages/web/i18n/zh-CN/account_bill.json +++ b/packages/web/i18n/zh-CN/account_bill.json @@ -36,6 +36,7 @@ "payment_method": "支付方式", "payway_coupon": "兑换码", "rerank": "结果重排", + "generate_sql": "生成 SQL", "save": "保存", "save_failed": "保存异常", "save_success": "保存成功", diff --git a/packages/web/i18n/zh-CN/admin.json b/packages/web/i18n/zh-CN/admin.json new file mode 100644 index 000000000000..46acf8a4c84b --- /dev/null +++ b/packages/web/i18n/zh-CN/admin.json @@ -0,0 +1,624 @@ +{ + "license_active_success": "激活成功", + "system_activation": "系统激活", + "system_activation_desc": "你需要使用 License 激活系统后才可继续使用。", + "domain_name": "当前域名为", + "input_license": "请输入 License", + "cancel": "取消", + "confirm": "确认", + "config_desc": "配置介绍", + "domain_invalid": "License 域名不合法", + "change_license": "变更授权", + "logout": "退出登录", + "expire_time": "过期时间", + "max_users": "最大用户数", + "max_apps": "最大应用数", + "max_datasets": "最大知识库数量", + "sso": "单点登录", + "pay": "支付系统", + "custom_templates": "自定义模板和系统工具", + "dataset_enhance": "知识库增强", + "unlimited": "不限制", + "data_dashboard": "数据面板", + "notification_management": "通知管理", + "log_management": "日志管理", + "user_management": "用户管理", + "user_info": "用户信息", + "team_management": "团队管理", + "plan_management": "套餐管理", + "payment_records": "支付记录", + "invoice_management": "开票管理", + "resource_management": "资源管理", + "app_management": "应用管理", + "dataset_management": "知识库管理", + "system_config": "系统配置", + "basic_config": "基础配置", + "feature_list": "功能清单", + "security_review": "安全审查", + "third_party_providers": "第三方提供商", + "user_config": "用户配置", + "plan_recharge": "套餐 & 充值", + "template_tools": "模板 & 工具", + "template_market": "模板市场", + "toolbox": "工具箱", + "audit_logs": "审计日志", + "first_page": "第一页", + "previous_page": "上一页", + "next_page": "下一页", + "last_page": "最后一页", + "page": "页", + "click_view_details": "点击查看详情", + "upload_image_failed": "上传图片失败", + "config_file": "配置文件", + "save": "保存", + "upload_profile_failed": "上传头像失败", + "associated_plugin_is_empty": "关联插件不能为空", + "config_success": "配置成功", + "confirm_delete_plugin": "确认删除该插件么?", + "delete_success": "删除成功", + "custom_plugin": "自定义插件", + "config": "配置", + "give_name": "取个名字", + "click_upload_avatar": "点击上传头像", + "app_name_empty": "应用名不能为空", + "description": "介绍", + "add_app_description": "为这个应用添加一个介绍", + "associated_plugins": "关联插件", + "search_plugin": "输入插件名或 appId 查找插件", + "attribute": "属性", + "author_name": "作者名称", + "default_system_name": "默认为系统名", + "is_enable": "是否启用", + "charge_token_fee": "是否收取 Token 费用", + "call_price": "调用价格 (n积分/次)", + "instructions": "使用说明", + "use_markdown_syntax": "使用 markdown 语法", + "update": "更新", + "create_plugin": "新建插件", + "delete_confirm_message": "删除后,其下资源将同步删除且不可恢复。是否确认删除?", + "group_management": "分组管理", + "total_groups": "共 {localGroups.length} 个分组", + "add": "添加", + "add_type": "添加类型", + "avatar_select_error": "头像选择异常", + "rename": "重命名", + "add_group": "添加分组", + "avatar_name": "头像 & 名称", + "click_set_avatar": "点击设置头像", + "group_name_empty": "分组名称不能为空", + "official": "官方", + "configured": "已配置", + "not_configured": "未配置", + "system_key_price": "系统密钥价格(n积分/次)", + "plugin_config": "{{name}} 配置", + "enable_toolset": "是否启用工具集", + "config_system_key": "是否配置系统密钥", + "tool_list": "工具列表", + "tool_name": "工具名", + "key_price": "密钥价格", + "continue": "继续", + "user_deleted_message": "这人被删了", + "missing_field": "缺少字段", + "team_not_exist": "团队不存在", + "subscription_exists": "已存在相同类型的订阅", + "subscription_not_exist": "订阅不存在", + "user_exist": "用户已存在", + "account_logout": "账号已经注销了", + "user_not_found": "找不到 user", + "user_not_exist": "用户不存在", + "update_failed": "更新失败", + "send_notification_success": "发送通知成功", + "user_password_error": "用户或密码错误!", + "invoice_not_found": "找不到发票", + "invoice_issued": "发票已开具", + "invoice_completed_notice": "您申请的发票已完成,请注意查收", + "invoice_generation_completed": "开票完成", + "order_not_exist": "订单不存在", + "order_unpaid": "订单未支付", + "order_invoiced_no_refund": "订单已开票,无法直接退款", + "order_insufficient_amount": "订单金额不足", + "type_empty": "type 不能为空", + "quantity_must_be_positive": "数量必须大于0", + "team_not_found": "未找到团队", + "dataset_training_rebuilding": "数据集正在训练或者重建中,请稍后再试", + "database_rebuilding_index": "数据库重建索引", + "type_not_support": "暂不支持该类型消息", + "send_verification_code_success": "发送验证码成功", + "normal": "正常", + "leave": "离开", + "deactivated": "已停用", + "account": "账号", + "username": "用户名", + "contact": "联系方式", + "department": "部门", + "join_time": "加入时间", + "update_time": "更新时间", + "status": "状态", + "team_num_limit_error": "仅限1个团队", + "organization_name": "组织名", + "amount": "金额", + "yuan": "元", + "pending_invoice_count": "待开票数量", + "go_to_view": "前去查看", + "new_invoice_application": "有新的开票申请", + "order_type_error": "订单类型错误", + "missing_key_params_update_bill_failed": "缺少关键参数,更新账单失败,请联系管理员", + "plugin_service_link_not_configured": "未配置插件服务链接", + "audit_record_table": "审计记录表", + "operator": "操作人员", + "operation_type": "操作类型", + "operation_time": "操作时间", + "operation_content": "操作内容", + "no_audit_records": "暂无审计记录~", + "audit_details": "审计详情", + "details": "详情", + "close": "关闭", + "total_users": "总用户数", + "registered_users": "注册用户数", + "payment_amount": "付费金额", + "order_count": "订单数", + "all": "全部", + "success": "成功", + "paid_teams": "付费团队数", + "total_conversations": "总对话数", + "total_sessions": "总会话数", + "avg_conversations_per_session": "每个会话平均对话数", + "points_consumed": "积分消耗", + "user_total": "用户总数", + "dataset_total": "知识库总数", + "app_total": "应用总数", + "statistics_data": "统计数据", + "traffic": "流量", + "payment": "付费", + "active": "活跃", + "cost": "成本", + "last_7_days": "近7天", + "last_30_days": "近30天", + "last_90_days": "近90天", + "last_180_days": "近180天", + "confirm_modify_system_announcement": "确认修改系统公告?", + "confirm_send_system_notification": "确认发送系统通知?", + "modify_success": "修改成功", + "modify_failed": "修改失败", + "send_success": "发送成功", + "send_failed": "发送失败", + "system_announcement_config": "系统公告配置", + "system_announcement_description": "设置该内容,会在用户登录系统后,通过弹窗形式进行强提示。用户关闭后,下次不再提示。只能设置1个该类型通知。支持 markdown 格式。", + "send_system_notification": "发送系统通知", + "confirm_send": "确认发送", + "send_notification_description": "为所有用户发送一个通知,不同等级通知,会有不同提示。", + "message_level": "消息等级", + "level_normal": "一般(仅发站内信)", + "level_important": "重要(站内信+登录通知)", + "level_urgent": "紧急(站内信+登录通知+邮件/短信提醒)", + "notification_title": "通知标题", + "notification_content": "通知内容", + "log_record_table": "日志记录表", + "log_search_placeholder": "请想要查找的日志内容,回车搜索", + "time": "时间", + "log_content": "日志内容", + "no_log_records": "暂无Log记录~", + "log_level": "日志等级", + "log_detail": "日志详情", + "log_message": "日志消息", + "login_success": "登录成功!", + "admin_login": "管理员登录", + "login_username": "用户名", + "login_password": "密码", + "login_button": "登录", + "app_list": "应用列表", + "app_name": "应用名", + "creator": "创建者", + "redirect": "跳转", + "app_no_records": "无应用记录~", + "app_details": "应用详情", + "app_id": "应用id", + "app_creator_id": "创建者 ID", + "dataset_list": "知识库列表", + "dataset_name": "知识库名", + "dataset_data_size": "数据量", + "dataset_vector_count": "向量总数", + "dataset_favorite_count": "收藏数", + "new": "新增", + "name": "名称", + "enable": "启用", + "redirect_link": "跳转链接", + "operation": "操作", + "add_sidebar_item": "新增侧边项", + "sidebar_item_name": "侧边项名", + "sidebar_item_name_empty": "侧边项名不能为空", + "redirect_link_empty": "跳转链接不能为空", + "edit_plan": "编辑 {{label}} 套餐", + "plan_name": "套餐名称", + "custom_plan_name_desc": "自定义套餐名,可覆盖原套餐名", + "monthly_price": "每月价格", + "max_team_members": "最大团队成员", + "max_app_count": "最大APP数量", + "max_dataset_count": "最大知识库数量", + "history_retention_days": "历史记录保存多少天", + "max_dataset_index_count": "最大知识库索引数量", + "monthly_ai_points": "每月 AI 积分", + "training_priority_high": "训练优先级(高的优先)", + "allow_site_sync": "允许使用站点同步", + "allow_team_operation_logs": "允许团队操作日志", + "click_configure_plan": "点击配置套餐", + "copy_success": "复制成功", + "delete_confirm": "确认删除该变量?", + "delete": "删除", + "workflow_variable_custom": "自定义工作流变量", + "field_name": "变量名", + "usage_url": "使用量查询地址", + "note": "说明", + "import_config": "导入配置", + "import_success_save": "导入成功,请点击保存", + "import_check_format": "请检查配置文件格式", + "import": "导入", + "get_config_error": "获取配置出错", + "save_success": "保存成功", + "save_failed": "保存失败", + "frontend_display_config": "前端展示配置", + "personalization_config": "个性化配置", + "global_script": "全局Script脚本", + "system_params": "系统参数", + "pdf_parse_config": "PDF 解析配置", + "usage_limits": "使用限制", + "sidebar_config": "侧边栏配置", + "system_name": "系统名", + "custom_api_domain": "自定义api域名", + "custom_api_domain_desc": "可以设置一个额外的api地址,不使用主站的地址,需配置域名的cname和ssl证书。", + "custom_share_link_domain": "自定义分享链接域名", + "custom_share_link_domain_desc": "可以设置一个额外的分享链接地址,不使用主站的地址,需配置域名的cname和ssl证书。", + "openapi_prefix": "OpenAPI 前缀", + "contact_popup": "联系弹窗", + "contact_popup_desc": "使用 Markdown 进行配置,配置之后,在网页中\"联系我们\"相关的内容,会提示填写的内容。", + "custom_api_doc_url": "自定义 api 文档地址", + "custom_openapi_doc_url": "自定义 openapi 文档地址", + "doc_url_note": "文档地址(加一个 / 结尾,否则会携带子路径跳转)", + "contribute_plugin_doc_url": "贡献插件文档地址", + "contribute_template_doc_url": "贡献模板市场文档地址", + "global_script_desc": "自定义 Script 脚本,可以全局插入(可以做站点流量监控之类的)", + "mcp_forward_service_url": "MCP 转发服务地址", + "mcp_forward_service_desc": "需要部署一个 MCP 转发服务,用于将 FastGPT 应用以MCP协议暴露,例如:http://localhost:3005", + "oneapi_url": "oneAPI地址(会覆盖环境变量配置的)", + "oneapi_url_desc": "oneAPI地址,可以使用 oneapi 来实现多模型接入", + "input_oneapi_url": "请输入 oneAPI 地址", + "oneapi_key": "OneAPI 密钥(会覆盖环境变量配置的)", + "input_oneapi_key": "请输入 OneAPI 密钥", + "dataset_parse_max_process": "知识库解析最大处理进程", + "dataset_index_max_process": "知识库索引最大处理进程", + "file_understanding_max_process": "文件理解模型最大处理进程", + "image_understanding_max_process": "图片理解模型最大处理进程", + "hnsw_ef_search": "HNSW ef_search", + "hnsw_ef_search_desc": "HNSW 参数。越大召回率越高,性能越差,默认为 100,具体可见:https://github.com/pgvector/pgvector", + "hnsw_max_scan_tuples": "HNSW max_scan_tuples", + "hnsw_max_scan_tuples_desc": "迭代搜索最大数量,越大召回率越高,性能越差,默认为 100000,具体可见:https://github.com/pgvector/pgvector", + "token_calc_max_process": "token计算最大进程(通常多少并发设置多少)", + "custom_pdf_parse_url": "自定义 PDF 解析地址", + "custom_pdf_parse_key": "自定义 PDF 解析密钥", + "custom_pdf_parse_timeout": "自定义 PDF 解析超时时间", + "doc2x_pdf_parse_key": "Doc2x pdf 解析密钥(比自定义 PDF 解析优先级低)", + "custom_pdf_parse_price": "自定义 PDF 解析价格(n 积分/页)", + "eval_config": "评测配置", + "eval_config_task_concurrency": "评测任务并发数", + "eval_config_task_concurrency_desc": "同时执行的评测任务数量", + "eval_config_case_concurrency": "评测用例执行并发数", + "eval_config_case_concurrency_desc": "单个评测任务中并发处理的用例数量", + "eval_config_case_max_retry": "评测用例最大重试次数", + "eval_config_case_max_retry_desc": "评测用例失败时的最大重试次数", + "eval_config_case_result_threshold": "评测判决默认阈值", + "eval_config_case_result_threshold_desc": "评测判决的默认阈值 (0-1之间,决定评测结果的正负)", + "eval_config_summary_concurrency": "评测报告生成并发数", + "eval_config_data_quality_concurrency": "评测数据集-数据质量并发数", + "eval_config_dataset_synthesize_concurrency": "评测数据集-数据合成并发数", + "eval_config_smart_generate_concurrency": "评测数据集-数据智能生成并发数", + "eval_config_maxStalledCount": "评测-任务卡滞最大重试次数", + "max_upload_files_per_time": "单次最多上传多少个文件", + "max_upload_files_per_time_desc": "用户上传知识库时,每次上传最多选择多少个文件", + "max_upload_file_size": "上传文件最大大小(M)", + "max_upload_file_size_desc": "用户上传知识库时,每个文件最大是多少。放大的话,需要注意网关也要设置得够大。", + "export_interval_minutes": "导出间隔时长(分钟)", + "site_sync_interval_minutes": "站点同步使用间隔时长(分钟)", + "mobile_sidebar_location": "移动端的侧边栏显示在账号 - 个人信息里", + "basic_features": "基础功能", + "third_party_knowledge_base": "第三方知识库", + "third_party_publish_channels": "第三方发布渠道", + "feature_display_config": "功能展示配置", + "display_team_sharing": "展示团队分享", + "display_chat_blank_page": "展示聊天空白页(都关闭即可)", + "display_invite_friends_activity": "展示邀请好友活动", + "frontend_compliance_notice": "前端是否展示合规提示文案", + "feishu_knowledge_base": "飞书知识库", + "feishu_knowledge_base_desc": "关闭后,创建数据库时不再显示飞书数据库", + "yuque_knowledge_base": "语雀知识库", + "yuque_knowledge_base_desc": "关闭后,创建数据库时不再显示语雀数据库", + "feishu_publish_channel": "飞书发布渠道", + "feishu_publish_channel_desc": "关闭后,发布渠道中不再显示飞书发布渠道", + "dingtalk_publish_channel": "钉钉发布渠道", + "dingtalk_publish_channel_desc": "关闭后,发布渠道中不再显示钉钉发布渠道", + "wechat_publish_channel": "公众号发布渠道", + "wechat_publish_channel_desc": "关闭后,发布渠道中不再显示公众号发布渠道", + "content_security_review": "内容安全审查", + "baidu_security_id": "百度安全 id", + "baidu_security_secret": "百度安全 secret", + "custom_security_check_url": "自定义安全校验 URL", + "baidu_security_register_desc": "注册百度安全校验账号,并创建对应应用。提供应用的 id 和 secret", + "custom_security_check_desc": "如果您有自己的安全校验服务,可以填写该地址,并在安全设置中开启自定义安全校验", + "plan_free": "免费版", + "plan_trial": "体验版", + "plan_team": "团队版", + "plan_enterprise": "企业版", + "subscription_plan": "订阅套餐", + "standard_subscription_plan": "标准订阅套餐", + "custom_plan_description": "自定义套餐说明", + "dataset_storage_cost_desc": "知识库存储费用(xx元/1000条/月)", + "extra_ai_points_cost_desc": "额外AI积分费用(xx元/1000积分/月)", + "payment_method": "支付方式", + "wechat_payment_config": "微信支付配置", + "alipay_payment_config": "支付宝支付配置", + "corporate_payment_message": "对公支付消息提示", + "enable_subscription_plan": "是否启用订阅套餐", + "custom_plan_page_description": "如果填写了该地址,会覆盖系统上套餐页面,会跳转到这个自定义页面,你可以在自定义页面里定义收费规则", + "wechat_payment_materials": "微信支付相关材料", + "wechat_payment_registration_guide": "自行注册微信支付,目前需要wx扫码支付", + "unused_field_placeholder": "没用到,随便填个", + "certificate_management_guide": "点管理证书进去看到", + "wechat_key_extraction_guide": "按微信教程拿到这几个文件,txt打开key", + "alipay_payment_materials": "支付宝支付相关材料", + "alipay_application_guide": "自行注册支付宝应用,目前需要开通电脑网站支付", + "alipay_certificate_encryption_guide": "点接口加签方式后选择证书加密方式,具体操作参考", + "application_public_key_certificate": "应用公钥证书", + "private_key_document_reference": "参考上面私钥获取文档", + "alipay_root_certificate": "支付宝根证书", + "alipay_public_key_certificate": "支付宝公钥证书", + "alipay_dateway": "支付宝网关", + "alipay_gateway_sandbox_note": "支付宝网关,注意测试使用的沙箱环境是\nhttps://openapi-sandbox.dl.alipaydev.com/gateway.do\n,而生成环境是\nhttps://openapi.alipay.com/gateway.do\n", + "alipay_endpoint_sandbox_note": "支付宝端点,注意测试使用的沙箱环境是\nhttps://openapi-sandbox.dl.alipaydev.com\n,而生成环境是\nhttps://openapi.alipay.com\n", + "message_notification": "消息提示", + "markdown_format_support": "支持markdown格式", + "third_party_account_config": "第三方账号配置", + "allow_user_account_config": "允许用户配置账号", + "view_documentation": "查看文档", + "openai_oneapi_account": "OpenAI/OneAPI 账号", + "laf_account": "laf 账号", + "input_laf_address": "请输入 laf 地址", + "multi_team_mode": "多团队模式", + "single_team_mode": "单团队模式", + "sync_mode": "同步模式", + "notification_login_settings": "通知 & 登陆设置", + "team_mode_settings": "团队模式设置", + "custom_user_system_config": "自定义用户系统配置", + "email_notification_config": "邮箱通知配置(注册、套餐通知)", + "aliyun_sms_config": "阿里云短信配置", + "aliyun_sms_template_code": "阿里云短信模板CODE(SMS_xxx)", + "wechat_service_login": "微信服务号登陆", + "github_login_config": "GitHub 登录配置", + "google_login_config": "Google 登陆配置", + "microsoft_login_config": "微软登陆配置", + "quick_login": "快速登陆(不推荐)", + "login_notifications_config": "通知登陆 & 设置", + "user_service_root_address": "用户服务根地址(末尾不加/)", + "sso_usage_guide": "具体用法请看: [SSO & 外部成员同步](https://doc.fastgpt.io/docs/guide/admin/sso/)", + "sso_login_button_title": "SSO 登录按钮标题", + "config_sso_login_button_title": "配置 SSO 登录按钮的标题", + "sso_login_button_icon": "SSO 登录按钮的图标", + "config_sso_login_button_icon": "配置 SSO 登录按钮的图标", + "sso_auto_redirect": "SSO 自动跳转", + "sso_auto_redirect_desc": "开启后,用户进入登录页面,将会自动触发 SSO 登录,无需手动点击。", + "email_smtp_address": "邮箱服务SMTP地址", + "email_smtp_address_note": "不同厂商不一样\nQQ: smtp.qq.com\ngmail: smtp.gmail.com", + "email_smtp_username": "邮箱服务SMTP用户名", + "email_smtp_username_example": "qq 邮箱为例,对应 qq 号", + "email_password": "邮箱 Password", + "email_smtp_auth_code": "SMTP 授权码", + "enable_email_registration": "是否开启邮箱注册", + "aliyun_access_key_guide": "阿里云短信参数\nhttps://dysms.console.aliyun.com/overview\n申请对应的签名和短信模板,提供:\nACCESSKEYID\nACCESSSECRET\n签名名称\n模板CODE,SM开头的", + "aliyun_secret_key": "阿里云账号的secret key", + "sms_signature": "短信签名", + "registration_account": "注册账号", + "registration_account_desc": "填写后,将会开启手机号注册", + "reset_password": "重置密码", + "reset_password_desc": "填写后,将会开启手机号找回密码", + "bind_notification_phone": "绑定通知手机号", + "bind_notification_phone_desc": "填写后,将会允许手机号绑定通知方式", + "subscription_expiring_soon": "订阅套餐即将过期", + "subscription_expiring_soon_desc": "填写后,套餐即将过期,会发送一个短信", + "free_user_cleanup_warning": "免费版用户清理警告", + "wechat_service_appid": "服务号的 Appid。微信服务号的验证地址填写:商业版域名", + "wechat_service_secret": "服务号的 Secret", + "register_one": "注册一个", + "provide": "提供", + "domain": "域名", + "microsoft_app_client_id": "对应 Microsoft 应用的「应用程序(客户端) ID」", + "microsoft_tenant_id": "对应 Microsoft 应用的「租户 ID」, 若使用默认的 common 可不用填写", + "custom_button_name": "自定义按钮名", + "custom_button_name_desc": "自定义按钮的名称,若不填写则使用默认的 Microsoft 按钮", + "simple_app": "简易应用", + "workflow": "工作流", + "plugin": "插件", + "folder": "文件夹", + "http_plugin": "HTTP 插件", + "toolset": "工具集", + "tool": "工具", + "hidden": "隐藏", + "select_json_file": "请选择 JSON 文件", + "confirm_delete_template": "确认删除该模板么?", + "upload_config_first": "请先上传配置文件", + "app_type_not_recognized": "未识别到应用类型", + "config_json_format_error": "配置文件 JSON 格式错误", + "template_update_success": "模板更新成功", + "template_create_success": "模板创建成功", + "template_config": "模板配置", + "json_serialize_failed": "JSON 序列化失败", + "get_app_type_failed": "获取应用类型失败", + "file_overwrite_content": "文件将覆盖当前内容", + "config_file_label": "配置文件", + "official_config": "官方配置", + "upload_file": "上传文件", + "paste_config_or_drag_json": "粘贴配置或拖入 JSON 文件", + "paste_config": "粘贴配置", + "app_type": "应用类型", + "auto_recognize": "自动识别", + "app_attribute_not_recognized": "未识别到应用属性", + "text": "文本", + "link": "链接", + "input_link": "请输入链接", + "settings_successful": "设置成功", + "configure_quick_templates": "配置快捷模板", + "search_apps": "搜索应用", + "selected_count": "已选: {{count}} / 3", + "category_management": "分类管理", + "total_categories": "共 {{length}} 个分类", + "add_category": "添加分类", + "category_name": "分类名", + "category_name_empty": "分类名不能为空", + "template_list": "模板列表", + "quick_template": "快捷模板", + "add_template": "添加模板", + "recommended": "推荐", + "no_templates": "暂无模板", + "add_attribute_first": "请先添加属性", + "add_plugin": "添加插件", + "token_points": "Token 积分", + "token_fee_description": "开启该开关后,用户使用该插件,需要支付插件中Token的积分,并且同时会收取调用积分", + "call_points": "调用积分", + "system_key": "系统密钥", + "system_key_description": "对于需要密钥的工具,您可为其配置系统密钥,用户可通过支付积分的方式使用系统密钥。", + "no_plugins": "暂无插件", + "invoice_application": "开票申请", + "search_user_placeholder": "请输入用户名,回车搜索", + "submit_status": "提交状态", + "submit_complete_time": "提交时间/完成时间", + "invoice_title": "抬头", + "waiting_for_invoice": "等待开票", + "completed": "已完成", + "confirm_invoice": "确认开票", + "no_invoice_records": "无开票记录~", + "invoice_details": "发票详情", + "invoice_amount": "开票金额", + "organization": "组织名称", + "unified_credit_code": "统一信用代码", + "company_address": "公司地址", + "company_phone": "公司电话", + "bank_name": "开户银行", + "bank_account": "开户账号", + "need_special_invoice": "是否需要专票", + "yes": "是", + "no": "否", + "email_address": "邮箱地址", + "invoice_file": "发票文件", + "click_download": "点击下载", + "operation_success": "操作成功", + "operation_failed": "操作失败", + "upload_invoice_pdf": "请上传发票的PDF文件", + "select_invoice_file": "选择发票文件", + "confirm_submission": "确认提交", + "balance_recharge": "余额充值", + "plan_subscription": "套餐订阅", + "knowledge_base_expansion": "知识库扩容", + "ai_points_package": "AI积分套餐", + "monthly": "按月", + "yearly": "按年", + "free": "免费", + "trial": "体验", + "team": "团队", + "enterprise": "企业", + "custom": "自定义", + "wechat": "微信", + "balance": "余额", + "alipay": "支付宝", + "corporate": "对公", + "redeem_code": "兑换码", + "search_user": "请输入用户名搜索", + "team_id": "团队ID", + "recharge_member_name": "充值的成员名", + "unpaid": "未支付", + "no_bill_records": "无账单记录~", + "order_details": "订单详情", + "order_number": "订单号", + "generation_time": "生成时间", + "order_type": "订单类型", + "subscription_period": "订阅周期", + "subscription_package": "订阅套餐", + "months": "月数", + "extra_knowledge_base_capacity": "额外知识库容量", + "extra_ai_points": "额外AI积分", + "time_error": "开始时间不能大于结束时间", + "points_error": "剩余积分不能大于总积分", + "add_success": "添加成功", + "add_package": "添加套餐", + "package_type": "套餐类型", + "basic_package": "基础套餐", + "start_time": "开始时间", + "required": "*必填", + "end_time": "结束时间", + "package_level": "套餐等级", + "total_points": "总积分", + "remaining_points": "剩余积分", + "price_yuan_for_record_only": "价格(元)-仅用于记录", + "price": "价格", + "update_success": "更新成功", + "edit": "编辑", + "edit_package": "编辑套餐", + "value_override_description": "下面的值会覆盖套餐配置,不填则会用套餐的标准值", + "team_member_limit": "团队成员上限", + "app_limit": "应用上限", + "knowledge_base_limit": "知识库上限", + "team_name": "团队名", + "points": "积分", + "start_end_time": "起止时间", + "version": "版", + "extra_knowledge_base": "额外知识库", + "no_package_records": "无套餐记录~", + "role": "权限", + "team_details": "团队详情", + "chage_success": "变更成功", + "team_edit": "团队编辑", + "team_list": "团队列表", + "create_time": "创建时间", + "no_team_data": "无团队记录~", + "add_user": "添加用户", + "password": "密码", + "password_requirements": "密码至少 8 位,且至少包含两种组合:数字、字母或特殊字符", + "account_deactivated": "账号已注销", + "edit_user": "编辑用户", + "user_status": "用户状态", + "confirm_deactivate_account": "确认注销该账号?会将该用户下关键资源删除,并修改其用户名成 xx-deleted", + "deactivate": "注销", + "no_user_records": "无用户记录~", + "user_details": "用户详情", + "system_incompatibility": "部分系统不兼容,导致页面崩溃。如果可以,请联系作者,反馈下具体操作和页面。大部分是 苹果 的 safari 浏览器导致,可以尝试更换 chrome 浏览器。", + "content_not_compliant": "您的内容不合规", + "baidu_content_security_check_exception": "百度内容安全校验异常", + "license_not_read": "未读取到 License", + "license_invalid": "License 不合法", + "license_expired": "License 已过期", + "license_content_error": "License 内容错误", + "system_not_activated": "系统未激活", + "exceed_max_users": "超过最大用户数", + "server_error": "服务器异常", + "request_error": "请求错误", + "unknown_error": "未知错误", + "token_expired_relogin": "token过期,重新登录", + "google_verification_result": "谷歌校验结果", + "abnormal_operation_environment": "您的操作环境存在异常,请刷新页面后重试或联系客服。", + "notify_expiring_packages": "通知即将过期的套餐", + "notify_free_users_cleanup": "通知免费版用户即将清理", + "bing_oauth_config_incomplete": "Bing OAuth配置不完整", + "request_limit_per_minute": "每分钟仅能请求次数", + "share_link_expired": "分享链接已过期", + "link_usage_limit_exceeded": "链接超出使用限制", + "authentication_failed": "身份校验失败", + "user_registration": "新用户注册", + "initial_password": "您的初始密码为", + "notification_too_frequent": "发送通知太频繁了", + "emergency_notification_requires_teamid": "紧急通知必须提供 teamId", + "send_sms_failed": "发送短信失败", + "fastgpt_user": "FastGPT用户", + "refund_failed": "退款失败", + "refund_request_failed": "退款请求失败", + "get_certificate_list_failed": "获取证书列表失败", + "platform_certificate_serial_mismatch": "平台证书序列号不相符", + "extra_knowledge_base_storage": "额外知识库存储", + "image_compression_error": "压缩图片异常", + "image_too_large": "图片太大了" +} diff --git a/packages/web/i18n/zh-CN/chat.json b/packages/web/i18n/zh-CN/chat.json index d21a51db7539..510b05682af6 100644 --- a/packages/web/i18n/zh-CN/chat.json +++ b/packages/web/i18n/zh-CN/chat.json @@ -129,5 +129,7 @@ "upload": "上传", "variable_invisable_in_share": "自定义变量在免登录链接中不可见", "view_citations": "查看引用", - "web_site_sync": "Web站点同步" + "web_site_sync": "Web站点同步", + "embedding_model_error": "向量模型出错,请核查模型配置信息", + "language_model_error":"请求大模型出错,请检查模型配置是否正确" } diff --git a/packages/web/i18n/zh-CN/common.json b/packages/web/i18n/zh-CN/common.json index ddc1d2597b5a..d0f85f40053f 100644 --- a/packages/web/i18n/zh-CN/common.json +++ b/packages/web/i18n/zh-CN/common.json @@ -1350,12 +1350,20 @@ "zoomin_tip_mac": "缩小 ⌘ -", "zoomout_tip": "放大 ctrl +", "zoomout_tip_mac": "放大 ⌘ +", - "no_database_connection": "还没有连接数据库", + "no_database_connection": "还没有连接数据库,", "click_config_database": "点击配置数据库", "core.dataset.table": "数据表", "core.dataset.search.mode.database": "数据库知识库检索", "core.dataset.search.mode.database desc": "使用向量检索查找数据库中可能相关的表和列", "core.dataset.training.databaseSchema mode": "数据库结构", - "core.dataset.import.databaseSchema Tip":"对数据库中的表信息进行自动处理,使其更利于检索,以提高SQL生成的准确率", - "database_search": "数据库搜索" -} + "core.dataset.import.databaseSchema Tip": "对数据库中的表信息进行自动处理,使其更利于检索,以提高SQL生成的准确率", + "database_search": "数据库搜索", + "annotation_answer": "标注答案", + "core.dataset.search.Database search": "数据库检索", + "search_model": "检索模型", + "search_model_desc": "用于生成可在数据库中检索的SQL语句,并进行检索与汇总,生成可用于对话的文本。", + "search_model_tip": "使用非推理模型、参数量大的模型效果更佳。", + "other_knowledge_base": "其他知识库", + "table_not_exist": "不存在", + "core.app.workflow.search_knowledge.database": "数据库" +} \ No newline at end of file diff --git a/packages/web/i18n/zh-CN/dashboard_evaluation.json b/packages/web/i18n/zh-CN/dashboard_evaluation.json index 8f24d2be32b7..cc05db7d3bd3 100644 --- a/packages/web/i18n/zh-CN/dashboard_evaluation.json +++ b/packages/web/i18n/zh-CN/dashboard_evaluation.json @@ -79,7 +79,7 @@ "create_new_dataset": "新建", "smart_generation": "智能生成", "file_import": "文件导入", - "confirm_delete_dataset": "确认删除该数据集吗?", + "confirm_delete_dataset": "确认删除该数据集?", "error_details": "异常详情", "status_queuing": "排队中", "status_parsing": "文件解析中", @@ -184,7 +184,6 @@ "custom_dimension": "自定义", "config_params": "配置参数", "score_aggregation_method": "分数聚合方式", - "score_aggregation_method_tip": "选择如何聚合多个维度的评分", "evaluation_dimensions": "评测维度", "dimension": "维度", "judgment_threshold": "判定阈值", @@ -251,7 +250,7 @@ "model_change_notice": "更改模型后将对后续评测的任务生效。", "evaluation_model": "评测模型", "select_evaluation_model": "请选择评测模型", - "evaluation_abnormal": "评测异常", + "evaluation_abnormal": "评测异常", "error_message": "报错信息", "file_parse_error": "文件解析异常", "delete_file": "删除文件", @@ -292,6 +291,85 @@ "please_select_dimension_first": "请先选择该维度", "model_evaluation_tip": "语言模型可判断实际回答和参考答案中的文本内容是否匹配;\n索引模型可将实际回答和参考答案转成向量,进一步评估语义相似性。", "create_new_dimension": "新建维度", - "retry_success": "重试成功", - "data_generation_error_count": "{{count}}条数据生成异常" -} + "retry_success": "重试成功", + "data_generation_error_count": "{{count}}条数据生成异常", + "builtin_answer_correctness_name": "回答准确度", + "builtin_answer_correctness_desc": "衡量生成的回答与参考答案在事实上的一致性,评估其是否准确无误。", + "builtin_answer_similarity_name": "语义相似度", + "builtin_answer_similarity_desc": "评估生成回答与参考答案在语义上的匹配程度,判断其是否表达了相同的核心信息。", + "builtin_answer_relevancy_name": "回答相关度", + "builtin_answer_relevancy_desc": "衡量生成回答与提问之间的契合度,判断回答是否紧扣问题。", + "builtin_faithfulness_name": "回答忠诚度", + "builtin_faithfulness_desc": "评估生成回答是否忠实于提供的上下文信息,判断是否存在虚构或不实内容。", + "builtin_context_recall_name": "检索匹配度", + "builtin_context_recall_desc": "衡量检索系统是否能够获取回答所需的所有关键信息,评估其检索的完整性。", + "builtin_context_precision_name": "检索精确度", + "builtin_context_precision_desc": "衡量检索内容中是否优先返回高价值信息,反映排序质量与信息密度。", + "join_evaluation_dataset": "加入评测数据集", + "not_join_evaluation_dataset": "不加入评测数据集", + "create_new_dataset_btn_text": "新建数据集", + "please_select_evaluation_dataset": "请选择评测数据集", + "join_knowledge_base": "加入知识库", + "all_data_with_count": "全部数据({{num}})", + "question_data_with_count": "问题数据({{num}})", + "error_data_with_count": "异常数据({{num}})", + "export_data": "导出", + "retry_action": "重试", + "basic_info": "基本信息", + "application": "应用", + "version": "版本", + "evaluation_dataset_name": "评测数据集", + "start_time": "开始时间", + "end_time": "结束时间", + "executor_name": "执行人", + "app_with_search_and_chat_recommendation": "评测应用包含知识库搜索和AI对话环节,已推荐使用 3 个维度进行评估", + "app_with_chat_recommendation": "评测应用包含AI对话环节,已推荐使用 1 个维度进行评估", + "app_with_search_recommendation": "评测应用包含知识库搜索环节,已推荐使用 2 个维度进行评估", + "no_dimensions_added": "还没有添加评测维度,", + "click_to_add": "点击添加", + "meets_expectation": "符合预期!", + "below_expectation": "低于预期分数!", + "summary_generation_error": "总结内容生成异常,", + "error_message_prefix": "报错信息:", + "summary_pending_generation": "总结内容待生成", + "summary_generating_content": "总结内容生成中", + "data_with_count": "数据({{data}})", + "search_placeholder": "搜索", + "detail_title": "详情", + "modify_dataset_simultaneously": "同时修改评测数据集", + "retry_button": "重试", + "edit_action": "编辑", + "delete_action": "删除", + "confirm_delete_data_in_task": "确认在当前任务中删除该数据?", + "view_full_response": "查看完整响应", + "abnormal_status": "异常", + "question_field": "问题", + "reference_answer_field": "参考答案", + "actual_answer_field": "实际回答", + "no_answer_available": "暂无回答", + "comprehensive_score_title": "综合评分", + "dimension_score_title": "维度评分", + "error_data_calculation_notice": "{{count}} 条数据执行异常,仅使用执行成功的数据来计算分数。", + "regenerate_summary_content": "重新生成总结内容", + "processing_status": "处理中...", + "view_results_after_completion": "完成后可查看评测结果", + "all_data_execution_error": "全部数据执行异常,", + "check_error_details": "请查看异常数据中的详细原因,", + "click_to_retry": "点击重试", + "comprehensive_score_weight_description": "按照指定权重计算测试数据全部维度的综合评分,可根据应用使用场景所关注的维度进行设置。", + "no_data_available": "暂无数据", + "question_column": "问题", + "comprehensive_score_column": "综合评分", + "error_info_column": "异常信息", + "request_failed": "请求失败", + "retry_request_submitted": "重试请求已提交", + "retry_failed": "重试失败", + "save_success": "保存成功", + "save_failed": "保存失败", + "summary_generation_request_submitted": "总结生成请求已提交", + "generate_summary_failed": "生成总结失败", + "export_failed": "导出失败", + "load_failed": "加载失败", + "export_success": "导出成功", + "no_dimension_data_cannot_generate_summary": "暂无维度数据,无法生成总结" +} \ No newline at end of file diff --git a/packages/web/i18n/zh-CN/database_client.json b/packages/web/i18n/zh-CN/database_client.json new file mode 100644 index 000000000000..de9523346aac --- /dev/null +++ b/packages/web/i18n/zh-CN/database_client.json @@ -0,0 +1,24 @@ +{ + "client_destory_error": "数据库连接客户端销毁失败", + "client_not_found": "数据库连接客户端未正确初始化", + "not_support_databaseType": "不支持的数据库类型", + "not_implemented_databaseType": "未实现的数据库客户端", + "connection_failed": "数据库连接失败,请检查连接配置", + "authentication_failed": "数据库认证失败,请检查用户名和密码以及数据库权限是否正确开放", + "database_not_exist": "数据库不存在,请检查数据库名称是否正确", + "database_port_error": "数据库端口错误,请检查端口是否正确", + "connection_address_error": "连接地址错误,请检查地址是否正确", + "connection_timeout": "数据库连接超时,请检查网络是否通畅", + "connection_refused": "数据库连接被拒绝,请检查数据库是否允许外部连接", + "connection_check_error": "连接测试失败,请检查网络是否通畅", + "connection_lost": "数据库连接被中断,请检查网络是否通畅", + "host_error": "连接地址错误,请检查地址是否正确", + "invalid_table_name": "无效的表名", + "fetch_info_error": "获取数据库信息失败", + "database_config_not_found": "未配置数据库连接信息,请先配置", + "table_not_found": "获取表信息失败,表不存在", + "illeagal_table_info": "非法的表信息,请检查表信息是否正确", + "op_unknown_database_error": "尝试操作您的数据库时发生未知错误,请检查数据库连接是否正常", + "dative_service_error": "连接Dative服务时发生错误,请检查配置是否正确", + "tableNamesDuplicate": "存在多个重复表名" +} \ No newline at end of file diff --git a/packages/web/i18n/zh-CN/dataset.json b/packages/web/i18n/zh-CN/dataset.json index f02de935815e..f7cdbed1c8c1 100644 --- a/packages/web/i18n/zh-CN/dataset.json +++ b/packages/web/i18n/zh-CN/dataset.json @@ -187,7 +187,7 @@ "split_sign_period": "句号", "split_sign_question": "问号", "split_sign_semicolon": "分号", - "start_sync_dataset_tip": "确实开始同步整个知识库?", + "start_sync_dataset_tip": "确定开始同步整个知识库?", "status_error": "运行异常", "sync_collection_failed": "同步集合错误,请检查是否能正常访问源文件", "sync_schedule": "定时同步", @@ -203,7 +203,7 @@ "tag.tags": "标签", "tag.total_tags": "共{{total}}个标签", "template_dataset": "模版导入", - "template_file_invalid": "模板文件格式不正确,应该是首列为 q,a,indexes 的 csv 文件", + "template_file_invalid": "模板文件格式不正确,应该是首行为 q,a,indexes 的 csv 文件", "template_mode": "模板导入", "the_knowledge_base_has_indexes_that_are_being_trained_or_being_rebuilt": "知识库有训练中或正在重建的索引", "total_num_files": "共 {{total}} 个文件", @@ -287,7 +287,7 @@ "column_enabled_tip": "启用后表示使用该列数据进行检索及回答", "connecting": "正在连接", "test_connectivity": "测试连通性", - "connect_and_next": "连接并进行下一步", + "connect_and_next": "连接并下一步", "connection_network_error": "连接失败,请检查网络连接", "validate_ip_tip": "该输入项禁止使用本地回环地址,如:localhost、127.x.x.x、0.0.0.0", "database": "数据库", @@ -319,5 +319,36 @@ "confirm_create_database_config": "确认创建数据库配置?", "database_config_updated": "数据库配置已更新", "data_index_column_description": "SQL字段描述索引", - "data_index_column_value":"SQL字段示例值索引" + "data_index_column_value":"SQL字段示例值索引", + "error_create_datasetcollection": "创建数据集失败,请联系管理员", + "data_source_refreshed": "数据源已刷新,", + "found": "发现", + "tables_with_column_changes": "发现 {{modifiedTablesCount}} 个数据表存在列的变更,", + "tables_not_exist": "发现 {{delTablesCount}} 个数据表已不存在,", + "please_check_latest_data": "请核查最新数据。", + "has_unfilled_content": "存在未填写的内容", + "has_column_changes": "存在列的变更", + "connection_success": "连接成功", + "no_changes_detected": "未出现信息变更。", + "tables_added": "新增 {{addedTables}} 个数据表", + "tables_modified": "发现 {{modifiedTables}} 个数据表存在列的变更,", + "tables_deleted": "发现 {{deletedTables}} 个数据表已不存在,", + "please_verify_data": "请核查最新数据。", + "changes_detected": "发现", + "new_columns_added_disabled": "个新增列(默认未启用),", + "columns_no_longer_exist": "个列已不存在,", + "check_latest_data": "请核查最新数据。", + "config": "配置", + "refresh_failed": "刷新失败", + "unknown_error": "未知错误", + "no_data_available": "暂无数据", + "database_sql_query": "数据库检索的 SQL 语句", + "search_result": "检索结果", + "no_search_result": "暂无检索结果", + "database_address_required": "数据库地址不能为空", + "ip_format_invalid_range": "IP地址格式不正确,每段数值应在0-255之间", + "database_address_format_invalid": "数据库地址格式不正确,请输入有效的IP地址或域名", + "tables_modified_and_deleted": "发现 {{modifiedTables}} 个数据表存在列的变更,{{deletedTables}} 个数据表已不存在,", + "tables_no_longer_exist_comma": "个数据表已不存在,", + "no_data_in_database": "该数据库中没有数据" } diff --git a/packages/web/i18n/zh-CN/evaluation.json b/packages/web/i18n/zh-CN/evaluation.json index 9a3cf426bc20..56ded9250ccd 100644 --- a/packages/web/i18n/zh-CN/evaluation.json +++ b/packages/web/i18n/zh-CN/evaluation.json @@ -4,7 +4,6 @@ "name_required": "名称必填", "name_too_long": "名称过长", "description_too_long": "描述过长", - "dataset_id_required": "数据集ID必填", "target_required": "评估目标必填", "target_invalid_config": "评估目标配置无效", "target_app_id_missing": "应用ID缺失", @@ -27,10 +26,8 @@ "invalid_status": "无效的状态", "invalid_state_transition": "无效的状态转换,只有排队状态或手动停止的评估可以启动", "only_running_can_stop": "只有运行中的评估可以停止", - "only_failed_can_retry": "只有失败的评估项可以重试", "item_no_error_to_retry": "评估项无错误可重试", - "target_output_required": "目标输出必填", - "evaluator_output_required": "评估器输出必填", + "target_execution_error": "评估目标执行错误", "dataset_load_failed": "数据集加载失败", "target_config_invalid": "目标配置无效", "evaluators_config_invalid": "评估器配置无效", @@ -39,7 +36,9 @@ "duplicate_dataset_name": "数据集名称重复", "no_data_in_collections": "集合中无数据", "update_failed": "更新失败", - "lock_acquisition_failed": "锁获取失败", + "task_system_error": "处理评估任务时发生系统错误", + "manually_stopped": "手动停止", + "evaluator_execution_errors": "评估器执行错误", "metric_not_found": "评估指标未找到", "metric_un_auth": "无权操作该评估指标", @@ -50,6 +49,7 @@ "metric_prompt_too_long": "指标提示词不能超过4000个字符", "metric_type_required": "指标类型不能为空", "metric_type_invalid": "指标类型无效", + "metric_name_invalid": "指标名称无效", "metric_builtin_cannot_modify": "内置指标不能修改", "metric_builtin_cannot_delete": "内置指标不能删除", "metric_id_required": "指标ID不能为空", @@ -76,7 +76,7 @@ "evaluator_service_unavailable": "评估器服务不可用", "evaluator_invalid_response": "评估器返回无效响应", "evaluator_network_error": "评估器网络连接错误", - + "eval_id_required": "评估任务ID不能为空", "summary_metrics_config_error": "指标配置错误", "summary_threshold_value_required": "阈值不能为空", @@ -113,10 +113,10 @@ "dataset_task_operation_failed": "任务操作失败", "dataset_task_delete_failed": "数据集任务删除失败", "fetch_failed_tasks_error": "获取失败任务列表失败", + "item_job_not_found": "未找到评估项作业", "file_id_required": "文件 ID 是必需的", "file_must_be_csv": "文件必须是 CSV 文件", "csv_invalid_structure": "CSV 文件结构无效", - "csv_too_many_rows": "CSV 文件包含的行数过多(最多 10,000 行)", "csv_parsing_error": "CSV 解析错误", "csv_no_data_rows": "CSV 文件不包含数据行", "count_must_be_greater_than_zero": "计数必须大于零", diff --git a/packages/web/i18n/zh-Hant/account_bill.json b/packages/web/i18n/zh-Hant/account_bill.json index aa3a92d61022..399c87368858 100644 --- a/packages/web/i18n/zh-Hant/account_bill.json +++ b/packages/web/i18n/zh-Hant/account_bill.json @@ -58,5 +58,6 @@ "unit_code_void": "統一信用代碼格式錯誤", "update": "更新", "yes": "是", - "yuan": "{{amount}}元" -} + "yuan": "{{amount}}元", + "generate_sql": "產生 SQL" +} \ No newline at end of file diff --git a/packages/web/i18n/zh-Hant/account_team.json b/packages/web/i18n/zh-Hant/account_team.json index 437d987510a2..314a35bd7fff 100644 --- a/packages/web/i18n/zh-Hant/account_team.json +++ b/packages/web/i18n/zh-Hant/account_team.json @@ -325,5 +325,11 @@ "log_create_evaluation_metric": "【{{name}}】創建了名為【{{metricName}}】的評測維度", "log_delete_evaluation_metric": "【{{name}}】删除了名為【{{metricName}}】的評測維度", "log_update_evaluation_metric": "【{{name}}】更新了名為【{{metricName}}】的評測維度", - "log_debug_evaluation_metric": "【{{name}}】調試了名為【{{metricName}}】的評測維度" -} + "log_debug_evaluation_metric": "【{{name}}】調試了名為【{{metricName}}】的評測維度", + "permission_evaluationCreate_Tip": "可以創建評估任務、評估指標和評估數據集", + "create_evaluation": "創建應用評測", + "export_evaluation": "導出應用評測數據", + "log_create_evaluation": "【{{name}}】創建了名為【{{appName}}】的【{{appType}}】的批量評測", + "log_export_evaluation": "【{{name}}】導出了名為【{{appName}}】的【{{appType}}】的評測數據", + "permission_evaluationCreate": "創建評估" +} \ No newline at end of file diff --git a/packages/web/i18n/zh-Hant/account_usage.json b/packages/web/i18n/zh-Hant/account_usage.json index d39a9280b996..215757a53c8b 100644 --- a/packages/web/i18n/zh-Hant/account_usage.json +++ b/packages/web/i18n/zh-Hant/account_usage.json @@ -58,5 +58,6 @@ "usage_detail": "使用詳細資訊", "user_type": "類型", "wecom": "企業微信", - "evaluation_summary_generation": "評估-總結生成" -} + "evaluation_summary_generation": "評估-總結生成", + "generate_answer": "產生應用回答" +} \ No newline at end of file diff --git a/packages/web/i18n/zh-Hant/admin.json b/packages/web/i18n/zh-Hant/admin.json new file mode 100644 index 000000000000..5e420ce4a672 --- /dev/null +++ b/packages/web/i18n/zh-Hant/admin.json @@ -0,0 +1,624 @@ +{ + "license_active_success": "激活成功", + "system_activation": "系統激活", + "system_activation_desc": "你需要使用 License 激活系統後才可繼續使用。", + "domain_name": "當前域名為", + "input_license": "請輸入 License", + "cancel": "取消", + "confirm": "確認", + "config_desc": "配置介紹", + "domain_invalid": "License 域名不合法", + "change_license": "變更授權", + "logout": "退出登錄", + "expire_time": "過期時間", + "max_users": "最大用戶數", + "max_apps": "最大應用數", + "max_datasets": "最大知識庫數量", + "sso": "單點登錄", + "pay": "支付系統", + "custom_templates": "自定義模板和系統工具", + "dataset_enhance": "知識庫增強", + "unlimited": "不限制", + "data_dashboard": "數據面板", + "notification_management": "通知管理", + "log_management": "日誌管理", + "user_management": "用戶管理", + "user_info": "用戶信息", + "team_management": "團隊管理", + "plan_management": "套餐管理", + "payment_records": "支付記錄", + "invoice_management": "開票管理", + "resource_management": "資源管理", + "app_management": "應用管理", + "dataset_management": "知識庫管理", + "system_config": "系統配置", + "basic_config": "基礎配置", + "feature_list": "功能清單", + "security_review": "安全審查", + "third_party_providers": "第三方提供商", + "user_config": "用戶配置", + "plan_recharge": "套餐 & 充值", + "template_tools": "模板 & 工具", + "template_market": "模板市場", + "toolbox": "工具箱", + "audit_logs": "審計日誌", + "first_page": "頁首", + "previous_page": "上一頁", + "next_page": "下一頁", + "last_page": "最後一頁", + "page": "頁", + "click_view_details": "點擊查看詳情", + "upload_image_failed": "上傳圖片失敗", + "config_file": "配置文件", + "save": "保存", + "upload_profile_failed": "上傳頭像失敗", + "associated_plugin_is_empty": "關聯插件不能為空", + "config_success": "配置成功", + "confirm_delete_plugin": "確認刪除該插件嗎?", + "delete_success": "刪除成功", + "custom_plugin": "自定義插件", + "config": "配置", + "give_name": "取個名字", + "click_upload_avatar": "點擊上傳頭像", + "app_name_empty": "應用名不能為空", + "description": "介紹", + "add_app_description": "為這個應用添加一個介紹", + "associated_plugins": "關聯插件", + "search_plugin": "輸入插件名或 appId 查找插件", + "attribute": "屬性", + "author_name": "作者名", + "default_system_name": "默認為系統名", + "is_enable": "是否啟用", + "charge_token_fee": "是否收取 Token 費用", + "call_price": "調用價格 (n積分/次)", + "instructions": "使用說明", + "use_markdown_syntax": "使用 markdown 語法", + "update": "更新", + "create_plugin": "新建插件", + "delete_confirm_message": "刪除後,其下資源將同步刪除且不可恢復。是否確認刪除?", + "group_management": "分組管理", + "total_groups": "共 {localGroups.length} 個分組", + "add": "添加", + "add_type": "添加類型", + "avatar_select_error": "頭像選擇異常", + "rename": "重命名", + "add_group": "添加分組", + "avatar_name": "頭像 & 名稱", + "click_set_avatar": "點擊設置頭像", + "group_name_empty": "分組名稱不能為空", + "official": "官方", + "configured": "已配置", + "not_configured": "未配置", + "system_key_price": "系統密鑰價格(n積分/次)", + "plugin_config": "{{name}} 配置", + "enable_toolset": "是否啟用工具集", + "config_system_key": "是否配置系統密鑰", + "tool_list": "工具列表", + "tool_name": "工具名", + "key_price": "密鑰價格", + "continue": "繼續", + "user_deleted_message": "這人被刪了", + "missing_field": "缺少字段", + "team_not_exist": "團隊不存在", + "subscription_exists": "已存在相同類型的訂閱", + "subscription_not_exist": "訂閱不存在", + "user_exist": "用戶已存在", + "account_logout": "賬號已經註銷了", + "user_not_found": "找不到用戶", + "user_not_exist": "用戶不存在", + "update_failed": "更新失敗", + "send_notification_success": "發送通知成功", + "user_password_error": "用戶或密碼錯誤!", + "invoice_not_found": "找不到發票", + "invoice_issued": "發票已開具", + "invoice_completed_notice": "您申請的發票已完成,請注意查收", + "invoice_generation_completed": "開票完成", + "order_not_exist": "訂單不存在", + "order_unpaid": "訂單未支付", + "order_invoiced_no_refund": "訂單已開票,無法直接退款", + "order_insufficient_amount": "訂單金額不足", + "type_empty": "類型不能為空", + "quantity_must_be_positive": "數量必須大於0", + "team_not_found": "未找到團隊", + "dataset_training_rebuilding": "數據集正在訓練或者重建中,請稍後再試", + "database_rebuilding_index": "數據庫重建索引", + "type_not_support": "暫不支持該類型消息", + "send_verification_code_success": "發送驗證碼成功", + "normal": "正常", + "leave": "離開", + "deactivated": "已停用", + "account": "賬號", + "username": "用戶名", + "contact": "聯繫方式", + "department": "部門", + "join_time": "加入時間", + "update_time": "更新時間", + "status": "狀態", + "team_num_limit_error": "僅限1個團隊", + "organization_name": "組織名", + "amount": "金額", + "yuan": "元", + "pending_invoice_count": "待開票數量", + "go_to_view": "前去查看", + "new_invoice_application": "有新的開票申請", + "order_type_error": "訂單類型錯誤", + "missing_key_params_update_bill_failed": "缺少關鍵參數,更新帳單失敗,請聯繫管理員", + "plugin_service_link_not_configured": "未配置插件服務鏈接", + "audit_record_table": "審計記錄表", + "operator": "操作人員", + "operation_type": "操作類型", + "operation_time": "操作時間", + "operation_content": "操作內容", + "no_audit_records": "暫無審計記錄~", + "audit_details": "審計詳情", + "details": "詳情", + "close": "關閉", + "total_users": "總用戶數", + "registered_users": "註冊用戶數", + "payment_amount": "支付金額", + "order_count": "訂單數", + "all": "全部", + "success": "成功", + "paid_teams": "付費團隊數", + "total_conversations": "總對話數", + "total_sessions": "總會話數", + "avg_conversations_per_session": "每個會話平均對話數", + "points_consumed": "積分消耗", + "user_total": "用戶總數", + "dataset_total": "知識庫總數", + "app_total": "應用總數", + "statistics_data": "統計數據", + "traffic": "流量", + "payment": "支付", + "active": "活躍", + "cost": "成本", + "last_7_days": "近7天", + "last_30_days": "近30天", + "last_90_days": "近90天", + "last_180_days": "近180天", + "confirm_modify_system_announcement": "確認修改系統公告?", + "confirm_send_system_notification": "確認發送系統通知?", + "modify_success": "修改成功", + "modify_failed": "修改失敗", + "send_success": "發送成功", + "send_failed": "發送失敗", + "system_announcement_config": "系統公告配置", + "system_announcement_description": "設置該內容,會在用戶登錄系統後,通過彈窗形式進行強提示。用戶關閉後,下次不再提示。只能設置1個該類型通知。支持 markdown 格式。", + "send_system_notification": "發送系統通知", + "confirm_send": "確認發送", + "send_notification_description": "為所有用戶發送一個通知,不同等級通知,會有不同提示。", + "message_level": "消息等級", + "level_normal": "一般(僅發站內信)", + "level_important": "重要(站內信+登錄通知)", + "level_urgent": "緊急(站內信+登錄通知+郵件/短信提醒)", + "notification_title": "通知標題", + "notification_content": "通知內容", + "log_record_table": "日誌記錄表", + "log_search_placeholder": "請想要查找的日誌內容,回車搜索", + "time": "時間", + "log_content": "日誌內容", + "no_log_records": "暫無Log記錄~", + "log_level": "日誌等級", + "log_detail": "日誌詳情", + "log_message": "日誌消息", + "login_success": "登錄成功!", + "admin_login": "管理員登錄", + "login_username": "用戶名", + "login_password": "密碼", + "login_button": "登錄", + "app_list": "應用列表", + "app_name": "應用名", + "creator": "創建者", + "redirect": "跳轉", + "app_no_records": "無應用記錄~", + "app_details": "應用詳情", + "app_id": "應用id", + "app_creator_id": "創建者 ID", + "dataset_list": "知識庫列表", + "dataset_name": "知識庫名", + "dataset_data_size": "數據量", + "dataset_vector_count": "向量總數", + "dataset_favorite_count": "收藏數", + "new": "新增", + "name": "名稱", + "enable": "啟用", + "redirect_link": "跳轉鏈接", + "operation": "操作", + "add_sidebar_item": "新增側邊項", + "sidebar_item_name": "側邊項名", + "sidebar_item_name_empty": "側邊項名不能為空", + "redirect_link_empty": "跳轉鏈接不能為空", + "edit_plan": "編輯 {{label}} 套餐", + "plan_name": "套餐名稱", + "custom_plan_name_desc": "自定義套餐名,可覆蓋原套餐名", + "monthly_price": "每月價格", + "max_team_members": "最大團隊成員", + "max_app_count": "最大APP數量", + "max_dataset_count": "最大知識庫數量", + "history_retention_days": "歷史記錄保存多少天", + "max_dataset_index_count": "最大知識庫索引數量", + "monthly_ai_points": "每月 AI 積分", + "training_priority_high": "訓練優先級(高的優先)", + "allow_site_sync": "允許使用站點同步", + "allow_team_operation_logs": "允許團隊操作日誌", + "click_configure_plan": "點擊配置套餐", + "copy_success": "複製成功", + "delete_confirm": "確認刪除該變量?", + "delete": "刪除", + "workflow_variable_custom": "自定義工作流變量", + "field_name": "變量名", + "usage_url": "使用量查詢地址", + "note": "說明", + "import_config": "導入配置", + "import_success_save": "導入成功,請點擊保存", + "import_check_format": "請檢查配置文件格式", + "import": "導入", + "get_config_error": "獲取配置出錯", + "save_success": "保存成功", + "save_failed": "保存失敗", + "frontend_display_config": "前端展示配置", + "personalization_config": "個性化配置", + "global_script": "全局Script腳本", + "system_params": "系統參數", + "pdf_parse_config": "PDF 解析配置", + "usage_limits": "使用限制", + "sidebar_config": "側邊欄配置", + "system_name": "系統名", + "custom_api_domain": "自定義api域名", + "custom_api_domain_desc": "可以設置一個額外的api地址,不使用主站的地址,需配置域名的cname和ssl證書。", + "custom_share_link_domain": "自定義分享鏈接域名", + "custom_share_link_domain_desc": "可以設置一個額外的分享鏈接地址,不使用主站的地址,需配置域名的cname和ssl證書。", + "openapi_prefix": "OpenAPI 前綴", + "contact_popup": "聯繫彈窗", + "contact_popup_desc": "使用 Markdown 進行配置,配置之後,在網頁中\"聯繫我們\"相關的內容,會提示填寫的內容。", + "custom_api_doc_url": "自定義 api 文檔地址", + "custom_openapi_doc_url": "自定義 openapi 文檔地址", + "doc_url_note": "文檔地址(加一個 / 結尾,否則會攜帶子路徑跳轉)", + "contribute_plugin_doc_url": "貢獻插件文檔地址", + "contribute_template_doc_url": "貢獻模板市場文檔地址", + "global_script_desc": "自定義 Script 腳本,可以全局插入(可以做站點流量監控之類的)", + "mcp_forward_service_url": "MCP 轉發服務地址", + "mcp_forward_service_desc": "需要部署一個 MCP 轉發服務,用於將 FastGPT 應用以MCP協議暴露,例如:http://localhost:3005", + "oneapi_url": "oneAPI地址(會覆蓋環境變量配置的)", + "oneapi_url_desc": "oneAPI地址,可以使用 oneapi 來實現多模型接入", + "input_oneapi_url": "請輸入 oneAPI 地址", + "oneapi_key": "OneAPI 密鑰(會覆蓋環境變量配置的)", + "input_oneapi_key": "請輸入 OneAPI 密鑰", + "dataset_parse_max_process": "知識庫解析最大處理進程", + "dataset_index_max_process": "知識庫索引最大處理進程", + "file_understanding_max_process": "文件理解模型最大處理進程", + "image_understanding_max_process": "圖片理解模型最大處理進程", + "hnsw_ef_search": "HNSW ef_search", + "hnsw_ef_search_desc": "HNSW 參數。越大召回率越高,性能越差,默認為 100,具體可見:https://github.com/pgvector/pgvector", + "hnsw_max_scan_tuples": "HNSW max_scan_tuples", + "hnsw_max_scan_tuples_desc": "迭代搜索最大數量,越大召回率越高,性能越差,默認為 100000,具體可見:https://github.com/pgvector/pgvector", + "token_calc_max_process": "token計算最大進程(通常多少並發設置多少)", + "custom_pdf_parse_url": "自定義 PDF 解析地址", + "custom_pdf_parse_key": "自定義 PDF 解析密鑰", + "custom_pdf_parse_timeout": "自定義 PDF 解析逾時時間", + "doc2x_pdf_parse_key": "Doc2x pdf 解析密鑰(比自定義 PDF 解析優先級低)", + "custom_pdf_parse_price": "自定義 PDF 解析價格(n 積分/頁)", + "eval_config": "評測配置", + "eval_config_task_concurrency": "評測任務並發數", + "eval_config_task_concurrency_desc": "同時執行的評測任務數量", + "eval_config_case_concurrency": "評測用例執行並發數", + "eval_config_case_concurrency_desc": "單個評測任務中並發處理的用例數量", + "eval_config_case_max_retry": "評測用例最大重試次數", + "eval_config_case_max_retry_desc": "評測用例失敗時的最大重試次數", + "eval_config_case_result_threshold": "評測判決默認閾值", + "eval_config_case_result_threshold_desc": "評測判決的默認闾值 (0-1之間,決定評測結果的正負)", + "eval_config_summary_concurrency": "評測報告生成並發數", + "eval_config_data_quality_concurrency": "評測數據集-數據質量評測並發數", + "eval_config_dataset_synthesize_concurrency": "評測數據集-數據合成並發數", + "eval_config_smart_generate_concurrency": "評測數據集-智能生成並發數", + "eval_config_maxStalledCount": "評測-任務卡滯最大重試次數", + "max_upload_files_per_time": "單次最多上傳多少個文件", + "max_upload_files_per_time_desc": "用戶上傳知識庫時,每次上傳最多選擇多少個文件", + "max_upload_file_size": "上傳文件最大大小(M)", + "max_upload_file_size_desc": "用戶上傳知識庫時,每個文件最大是多少。放大的話,需要注意網關也要設置得夠大。", + "export_interval_minutes": "導出間隔時長(分鐘)", + "site_sync_interval_minutes": "站點同步使用間隔時長(分鐘)", + "mobile_sidebar_location": "移動端的側邊欄顯示在賬號 - 個人信息里", + "basic_features": "基礎功能", + "third_party_knowledge_base": "第三方知識庫", + "third_party_publish_channels": "第三方發布渠道", + "feature_display_config": "功能展示配置", + "display_team_sharing": "展示團隊分享", + "display_chat_blank_page": "展示聊天空白頁(都關閉即可)", + "display_invite_friends_activity": "展示邀請好友活動", + "frontend_compliance_notice": "前端是否展示合規提示文案", + "feishu_knowledge_base": "飛書知識庫", + "feishu_knowledge_base_desc": "關閉後,創建數據庫時不再顯示飛書數據庫", + "yuque_knowledge_base": "語雀知識庫", + "yuque_knowledge_base_desc": "關閉後,創建數據庫時不再顯示語雀數據庫", + "feishu_publish_channel": "飛書發布渠道", + "feishu_publish_channel_desc": "關閉後,發布渠道中不再顯示飛書發布渠道", + "dingtalk_publish_channel": "釘釘發布渠道", + "dingtalk_publish_channel_desc": "關閉後,發布渠道中不再顯示釘釘發布渠道", + "wechat_publish_channel": "公眾號發布渠道", + "wechat_publish_channel_desc": "關閉後,發布渠道中不再顯示公眾號發布渠道", + "content_security_review": "內容安全審查", + "baidu_security_id": "百度安全 id", + "baidu_security_secret": "百度安全 secret", + "custom_security_check_url": "自定義安全校驗 URL", + "baidu_security_register_desc": "註冊百度安全校驗賬號,並創建對應應用。提供應用的 id 和 secret", + "custom_security_check_desc": "如果您有自己的安全校驗服務,可以填寫該地址,並在安全設置中開啟自定義安全校驗", + "plan_free": "免費版", + "plan_trial": "體驗版", + "plan_team": "團隊版", + "plan_enterprise": "企業版", + "subscription_plan": "訂閱套餐", + "standard_subscription_plan": "標準訂閱套餐", + "custom_plan_description": "自定義套餐說明", + "dataset_storage_cost_desc": "知識庫存儲費用(xx元/1000條/月)", + "extra_ai_points_cost_desc": "額外AI積分費用(xx元/1000積分/月)", + "payment_method": "支付方式", + "wechat_payment_config": "微信支付配置", + "alipay_payment_config": "支付寶支付配置", + "corporate_payment_message": "對公支付消息提示", + "enable_subscription_plan": "是否啟用訂閱套餐", + "custom_plan_page_description": "如果填寫了該地址,會覆蓋系統上套餐頁面,會跳轉到這個自定義頁面,你可以在自定義頁面裡定義收費規則", + "wechat_payment_materials": "微信支付相關材料", + "wechat_payment_registration_guide": "自行註冊微信支付,目前需要wx掃碼支付", + "unused_field_placeholder": "沒用到,隨便填個", + "certificate_management_guide": "點管理證書進去看到", + "wechat_key_extraction_guide": "按微信教程拿到這幾個文件,txt打開key", + "alipay_payment_materials": "支付寶支付相關材料", + "alipay_application_guide": "自行註冊支付寶應用,目前需要開通電腦網站支付", + "alipay_certificate_encryption_guide": "點接口加簽方式後選擇證書加密方式,具體操作參考", + "application_public_key_certificate": "應用公鑰證書", + "private_key_document_reference": "參考上面私鑰獲取文檔", + "alipay_root_certificate": "支付寶根證書", + "alipay_public_key_certificate": "支付寶公鑰證書", + "alipay_dateway": "支付寶網關", + "alipay_gateway_sandbox_note": "支付寶網關,注意測試使用的沙箱環境是\nhttps://openapi-sandbox.dl.alipaydev.com/gateway.do\n,而生成環境是\nhttps://openapi.alipay.com/gateway.do\n", + "alipay_endpoint_sandbox_note": "支付寶端點,注意測試使用的沙箱環境是\nhttps://openapi-sandbox.dl.alipaydev.com\n,而生成環境是\nhttps://openapi.alipay.com\n", + "message_notification": "消息提示", + "markdown_format_support": "支持markdown格式", + "third_party_account_config": "第三方賬號配置", + "allow_user_account_config": "允許用戶配置賬號", + "view_documentation": "查看文檔", + "openai_oneapi_account": "OpenAI/OneAPI 賬號", + "laf_account": "laf 賬號", + "input_laf_address": "請輸入 laf 地址", + "multi_team_mode": "多團隊模式", + "single_team_mode": "單團隊模式", + "sync_mode": "同步模式", + "notification_login_settings": "通知 & 登錄設置", + "team_mode_settings": "團隊模式設置", + "custom_user_system_config": "自定義用戶系統配置", + "email_notification_config": "郵箱通知配置(註冊、套餐通知)", + "aliyun_sms_config": "阿里雲短信配置", + "aliyun_sms_template_code": "阿里雲短信模板CODE(SMS_xxx)", + "wechat_service_login": "微信服務號登錄", + "github_login_config": "GitHub 登錄配置", + "google_login_config": "Google 登錄配置", + "microsoft_login_config": "微軟登錄配置", + "quick_login": "快速登錄(不推薦)", + "login_notifications_config": "通知登錄 & 設置", + "user_service_root_address": "用戶服務根地址(末尾不加/)", + "sso_usage_guide": "具體用法請看: [SSO & 外部成員同步](https://doc.fastgpt.io/docs/guide/admin/sso/)", + "sso_login_button_title": "SSO 登錄按鈕標題", + "config_sso_login_button_title": "配置 SSO 登錄按鈕的標題", + "sso_login_button_icon": "SSO 登錄按鈕的圖標", + "config_sso_login_button_icon": "配置 SSO 登錄按鈕的圖標", + "sso_auto_redirect": "SSO 自動跳轉", + "sso_auto_redirect_desc": "開啟後,用戶進入登錄頁面,將會自動觸發 SSO 登錄,無需手動點擊。", + "email_smtp_address": "郵箱服務SMTP地址", + "email_smtp_address_note": "不同廠商不一樣\nQQ: smtp.qq.com\ngmail: smtp.gmail.com", + "email_smtp_username": "郵箱服務SMTP用戶名", + "email_smtp_username_example": "qq 郵箱為例,對應 qq 號", + "email_password": "郵箱 密碼", + "email_smtp_auth_code": "SMTP 授權碼", + "enable_email_registration": "是否開啟郵箱註冊", + "aliyun_access_key_guide": "阿里雲短信參數\nhttps://dysms.console.aliyun.com/overview\n申請對應的簽名和短信模板,提供:\nACCESSKEYID\nACCESSSECRET\n簽名名稱\n模板CODE,SM開頭的", + "aliyun_secret_key": "阿里雲賬號的secret key", + "sms_signature": "短信簽名", + "registration_account": "註冊賬號", + "registration_account_desc": "填寫後,將會開啟手機號註冊", + "reset_password": "重置密碼", + "reset_password_desc": "填寫後,將會開啟手機號找回密碼", + "bind_notification_phone": "綁定通知手機號", + "bind_notification_phone_desc": "填寫後,將會允許手機號綁定通知方式", + "subscription_expiring_soon": "訂閱套餐即將過期", + "subscription_expiring_soon_desc": "填寫後,套餐即將過期,會發送一個短信", + "free_user_cleanup_warning": "免費版用戶清理警告", + "wechat_service_appid": "服務號的 Appid。微信服務號的驗證地址填寫:商業版域名//api/support/user/account/login/wx/callback", + "wechat_service_secret": "服務號的 Secret", + "register_one": "註冊一個", + "provide": "提供", + "domain": "域名", + "microsoft_app_client_id": "對應 Microsoft 應用的「應用程序(客戶端) ID」", + "microsoft_tenant_id": "對應 Microsoft 應用的「租戶 ID」, 若使用默認的 common 可不用填寫", + "custom_button_name": "自定義按鈕名", + "custom_button_name_desc": "自定義按鈕的名稱,若不填寫則使用默認的 Microsoft 按鈕", + "simple_app": "簡易應用", + "workflow": "工作流", + "plugin": "插件", + "folder": "文件夾", + "http_plugin": "HTTP 插件", + "toolset": "工具集", + "tool": "工具", + "hidden": "隱藏", + "select_json_file": "請選擇 JSON 文件", + "confirm_delete_template": "確認刪除該模板嗎?", + "upload_config_first": "請先上傳配置文件", + "app_type_not_recognized": "未識別到應用類型", + "config_json_format_error": "配置文件 JSON 格式錯誤", + "template_update_success": "模板更新成功", + "template_create_success": "模板創建成功", + "template_config": "模板配置", + "json_serialize_failed": "JSON 序列化失敗", + "get_app_type_failed": "獲取應用類型失敗", + "file_overwrite_content": "文件將覆蓋當前內容", + "config_file_label": "配置文件", + "official_config": "官方配置", + "upload_file": "上傳文件", + "paste_config_or_drag_json": "粘貼配置或拖入 JSON 文件", + "paste_config": "粘貼配置", + "app_type": "應用類型", + "auto_recognize": "自動識別", + "app_attribute_not_recognized": "未識別到應用屬性", + "text": "文本", + "link": "鏈接", + "input_link": "請輸入鏈接", + "settings_successful": "設置成功", + "configure_quick_templates": "配置快捷模板", + "search_apps": "搜索應用", + "selected_count": "已選: {{count}} / 3", + "category_management": "分類管理", + "total_categories": "共 {{length}} 個分類", + "add_category": "添加分類", + "category_name": "分類名", + "category_name_empty": "分類名不能為空", + "template_list": "模板列表", + "quick_template": "快捷模板", + "add_template": "添加模板", + "recommended": "推薦", + "no_templates": "暫無模板", + "add_attribute_first": "請先添加屬性", + "add_plugin": "添加插件", + "token_points": "Token 積分", + "token_fee_description": "開啟該開關後,用戶使用該插件,需要支付插件中Token的積分,並且同時會收取調用積分", + "call_points": "調用積分", + "system_key": "系統密鑰", + "system_key_description": "對於需要密鑰的工具,您可為其配置系統密鑰,用戶可透過支付積分的方式使用系統密鑰。", + "no_plugins": "暫無插件", + "invoice_application": "開票申請", + "search_user_placeholder": "請輸入用戶名,回車搜索", + "submit_status": "提交狀態", + "submit_complete_time": "提交時間/完成時間", + "invoice_title": "抬頭", + "waiting_for_invoice": "等待開票", + "completed": "已完成", + "confirm_invoice": "確認開票", + "no_invoice_records": "無開票記錄~", + "invoice_details": "發票詳情", + "invoice_amount": "開票金額", + "organization": "組織名稱", + "unified_credit_code": "統一信用代碼", + "company_address": "公司地址", + "company_phone": "公司電話", + "bank_name": "開戶銀行", + "bank_account": "開戶賬號", + "need_special_invoice": "是否需要專票", + "yes": "是", + "no": "否", + "email_address": "郵箱地址", + "invoice_file": "發票文件", + "click_download": "點擊下載", + "operation_success": "操作成功", + "operation_failed": "操作失敗", + "upload_invoice_pdf": "請上傳發票的PDF文件", + "select_invoice_file": "選擇發票文件", + "confirm_submission": "確認提交", + "balance_recharge": "餘額充值", + "plan_subscription": "套餐訂閱", + "knowledge_base_expansion": "知識庫擴容", + "ai_points_package": "AI積分套餐", + "monthly": "按月", + "yearly": "按年", + "free": "免費", + "trial": "體驗", + "team": "團隊", + "enterprise": "企業", + "custom": "自定義", + "wechat": "微信", + "balance": "餘額", + "alipay": "支付寶", + "corporate": "對公", + "redeem_code": "兌換碼", + "search_user": "請輸入用戶名搜索", + "team_id": "團隊ID", + "recharge_member_name": "充值的成員名", + "unpaid": "未支付", + "no_bill_records": "無賬單記錄~", + "order_details": "訂單詳情", + "order_number": "訂單號", + "generation_time": "生成時間", + "order_type": "訂單類型", + "subscription_period": "訂閱周期", + "subscription_package": "訂閱套餐", + "months": "月數", + "extra_knowledge_base_capacity": "額外知識庫容量", + "extra_ai_points": "額外AI積分", + "time_error": "開始時間不能大於結束時間", + "points_error": "剩餘積分不能大於總積分", + "add_success": "添加成功", + "add_package": "添加套餐", + "package_type": "套餐類型", + "basic_package": "基礎套餐", + "start_time": "開始時間", + "required": "*必填", + "end_time": "結束時間", + "package_level": "套餐級別", + "total_points": "總積分", + "remaining_points": "剩餘積分", + "price_yuan_for_record_only": "價格(元)-僅用於記錄", + "price": "價格", + "update_success": "更新成功", + "edit": "編輯", + "edit_package": "編輯套餐", + "value_override_description": "下面的值會覆蓋套餐配置,不填則會用套餐的標準值", + "team_member_limit": "團隊成員上限", + "app_limit": "應用上限", + "knowledge_base_limit": "知識庫上限", + "team_name": "團隊名", + "points": "積分", + "start_end_time": "起止時間", + "version": "版", + "extra_knowledge_base": "額外知識庫", + "no_package_records": "無套餐記錄~", + "role": "權限", + "team_details": "團隊詳情", + "chage_success": "變更成功", + "team_edit": "團隊編輯", + "team_list": "團隊列表", + "create_time": "創建時間", + "no_team_data": "無團隊記錄~", + "add_user": "添加用戶", + "password": "密碼", + "password_requirements": "密碼至少 8 位,且至少包含兩種組合:數字、字母或特殊字符", + "account_deactivated": "賬號已註銷", + "edit_user": "編輯用戶", + "user_status": "用戶狀態", + "confirm_deactivate_account": "確認註銷該賬號?會將該用戶下關鍵資源刪除,並修改其用戶名成 xx-deleted", + "deactivate": "註銷", + "no_user_records": "無用戶記錄~", + "user_details": "用戶詳情", + "system_incompatibility": "部分系統不兼容,導致頁面崩潰。如果可以,請聯繫作者,反饋下具體操作和頁面。大部分是 蘋果 的 safari 瀏覽器導致,可以嘗試更換 chrome 瀏覽器。", + "content_not_compliant": "您的內容不合規", + "baidu_content_security_check_exception": "百度內容安全校驗異常", + "license_not_read": "未讀取到 License", + "license_invalid": "License 不合法", + "license_expired": "License 已過期", + "license_content_error": "License 內容錯誤", + "system_not_activated": "系統未激活", + "exceed_max_users": "超過最大用戶數", + "server_error": "服務器異常", + "request_error": "請求錯誤", + "unknown_error": "未知錯誤", + "token_expired_relogin": "token過期,重新登錄", + "google_verification_result": "谷歌校驗結果", + "abnormal_operation_environment": "您的操作環境存在異常,請刷新頁面後重試或聯繫客服。", + "notify_expiring_packages": "通知即將過期的套餐", + "notify_free_users_cleanup": "通知免費版用戶即將清理", + "bing_oauth_config_incomplete": "Bing OAuth配置不完整", + "request_limit_per_minute": "每分鐘僅能請求次數", + "share_link_expired": "分享鏈接已過期", + "link_usage_limit_exceeded": "鏈接超出使用限制", + "authentication_failed": "身份校驗失敗", + "user_registration": "新用戶註冊", + "initial_password": "您的初始密碼為", + "notification_too_frequent": "發送通知太頻繁了", + "emergency_notification_requires_teamid": "緊急通知必須提供 teamId", + "send_sms_failed": "發送短信失敗", + "fastgpt_user": "FastGPT用戶", + "refund_failed": "退款失敗", + "refund_request_failed": "退款請求失敗", + "get_certificate_list_failed": "獲取證書列表失敗", + "platform_certificate_serial_mismatch": "平台證書序列號不相符", + "extra_knowledge_base_storage": "額外知識庫存儲", + "image_compression_error": "壓縮圖片異常", + "image_too_large": "圖片太大了" +} diff --git a/packages/web/i18n/zh-Hant/app.json b/packages/web/i18n/zh-Hant/app.json index 9b165207a0fa..bccf5902c0d4 100644 --- a/packages/web/i18n/zh-Hant/app.json +++ b/packages/web/i18n/zh-Hant/app.json @@ -352,6 +352,14 @@ "files_cascader_select_first": "請先選擇知識庫", "files_cascader_dataset_empty": "該知識庫資料集為空", "select_join_location": "選擇加入位置", - "no_data_for_smart_generate": "該知識庫中沒有可用於智能生成的數據" - -} + "no_data_for_smart_generate": "該知識庫中沒有可用於智能生成的數據", + "logs_bad_feedback": "點踩", + "logs_source_count": "渠道用戶", + "logs_timespan_day": "按日", + "logs_timespan_month": "按月", + "logs_timespan_quarter": "按季", + "logs_timespan_week": "按週", + "logs_total_avg_duration": "平均時長", + "logs_total_feedback": "共 {{goodFeedBack}} 讚 | 共 {{badFeedBack}} 踩", + "logs_user_callback": "用戶回饋" +} \ No newline at end of file diff --git a/packages/web/i18n/zh-Hant/chat.json b/packages/web/i18n/zh-Hant/chat.json index 2f2443e463f5..6e06535ece6d 100644 --- a/packages/web/i18n/zh-Hant/chat.json +++ b/packages/web/i18n/zh-Hant/chat.json @@ -126,5 +126,10 @@ "upload": "上傳", "variable_invisable_in_share": "自定義變數在免登入連結中不可見", "view_citations": "檢視引用", - "web_site_sync": "網站同步" -} + "web_site_sync": "網站同步", + "embedding_model_error": "向量模型出錯,請核對模型配置資訊", + "language_model_error": "請求大模型出錯,請檢查模型配置是否正確", + "setting.copyright.save_success": "Logo 儲存成功", + "setting.home.available_tools": "可用工具", + "setting.share": "分享" +} \ No newline at end of file diff --git a/packages/web/i18n/zh-Hant/common.json b/packages/web/i18n/zh-Hant/common.json index 8746c4b2f2de..b51f9d123362 100644 --- a/packages/web/i18n/zh-Hant/common.json +++ b/packages/web/i18n/zh-Hant/common.json @@ -146,7 +146,6 @@ "code_error.error_code.502": "閘道錯誤", "code_error.error_code.503": "伺服器過載或維護中", "code_error.error_code.504": "閘道逾時", - "code_error.error_code[429]": "請求過於頻繁", "code_error.error_message.403": "憑證錯誤", "code_error.error_message.510": "帳戶餘額不足", "code_error.error_message.511": "無權操作此模型", @@ -807,7 +806,6 @@ "dataset_text_model_tip": "用於知識庫預處理階段的文字處理,例如自動補充索引、問答對提取。", "date_12_months": "12個月", "date_1_month": "1個月", - "date_3 months": "3個月", "date_6_months": "6個月", "deep_rag_search": "深度搜尋", "delete_api": "確認刪除此 API 金鑰?\n刪除後該金鑰將立即失效,對應的對話記錄不會被刪除,請確認!", @@ -1347,6 +1345,25 @@ "zoomin_tip_mac": "縮小 ⌘ -", "zoomout_tip": "放大 ctrl +", "zoomout_tip_mac": "放大 ⌘ +", - "no_database_connection": "還沒有連接數據庫", - "click_config_database": "點擊配置數據庫" -} + "no_database_connection": "還沒有連接數據庫,", + "click_config_database": "點擊配置數據庫", + "annotation_answer": "標註答案", + "core.dataset.search.Database search": "資料庫檢索", + "search_model": "檢索模型", + "search_model_desc": "用於產生可在資料庫中檢索的SQL語句,並進行檢索與匯總,產生可用於對話的文字。", + "search_model_tip": "使用非推理模型、參數量大的模型效果更佳。", + "other_knowledge_base": "其他知識庫", + "table_not_exist": "不存在", + "core.app.workflow.search_knowledge.database": "資料庫", + "code_error.error_code.429": "請求過於頻繁", + "core.chat.retry": "重新產生", + "date_3_months": "3個月", + "database_search": "資料庫搜尋", + "core.chat.logs.evaluation": "評估測試", + "support.wallet.subscription.eval_items_count": "單次評測資料條數: {{count}} 條", + "core.dataset.table": "資料表", + "core.dataset.search.mode.database": "資料庫知識庫檢索", + "core.dataset.search.mode.database desc": "使用向量檢索查找資料庫中可能相關的表和列", + "core.dataset.training.databaseSchema mode": "資料庫結構", + "core.dataset.import.databaseSchema Tip": "對資料庫中的表資訊進行自動處理,使其更利於檢索,以提高SQL產生的準確率" +} \ No newline at end of file diff --git a/packages/web/i18n/zh-Hant/dashboard_evaluation.json b/packages/web/i18n/zh-Hant/dashboard_evaluation.json index 3a5405f1b819..e6a8b6d5839f 100644 --- a/packages/web/i18n/zh-Hant/dashboard_evaluation.json +++ b/packages/web/i18n/zh-Hant/dashboard_evaluation.json @@ -72,7 +72,7 @@ "create_new_dataset": "新建", "smart_generation": "智能生成", "file_import": "文件導入", - "confirm_delete_dataset": "確認刪除該數據集嗎?", + "confirm_delete_dataset": "確認刪除該數據集?", "error_details": "異常詳情", "status_queuing": "排隊中", "status_parsing": "文件解析中", @@ -177,7 +177,6 @@ "custom_dimension": "自定義", "config_params": "配置參數", "score_aggregation_method": "分數聚合方式", - "score_aggregation_method_tip": "選擇如何聚合多個維度的評分", "evaluation_dimensions": "評測維度", "dimension": "維度", "judgment_threshold": "判定閾值", @@ -285,6 +284,92 @@ "please_select_dimension_first": "請先選擇該維度", "model_evaluation_tip": "語言模型可判斷實際回答和參考答案中的文本內容是否匹配;\n索引模型可將實際回答和參考答案轉成向量,進一步評估語義相似性。", "create_new_dimension": "新建維度", - "retry_success": "重試成功", - "data_generation_error_count": "{{count}}條數據生成異常" -} + "retry_success": "重試成功", + "data_generation_error_count": "{{count}}條數據生成異常", + "builtin_answer_correctness_name": "回答準確度", + "builtin_answer_correctness_desc": "衡量生成的回答與參考答案在事實上的一致性,評估其是否準確無誤。", + "builtin_answer_similarity_name": "語義相似度", + "builtin_answer_similarity_desc": "評估生成回答與參考答案在語義上的匹配程度,判斷其是否表達了相同的核心信息。", + "builtin_answer_relevancy_name": "回答相關度", + "builtin_answer_relevancy_desc": "衡量生成回答與提問之間的契合度,判斷回答是否緊扣問題。", + "builtin_faithfulness_name": "回答忠誠度", + "builtin_faithfulness_desc": "評估生成回答是否忠實於提供的上下文信息,判斷是否存在虛構或不實內容。", + "builtin_context_recall_name": "檢索匹配度", + "builtin_context_recall_desc": "衡量檢索系統是否能夠獲取回答所需的所有關鍵信息,評估其檢索的完整性。", + "builtin_context_precision_name": "檢索精確度", + "builtin_context_precision_desc": "衡量檢索內容中是否優先返回高價值信息,反映排序質量與信息密度。", + "join_evaluation_dataset": "加入評測數據集", + "not_join_evaluation_dataset": "不加入評測數據集", + "create_new_dataset_btn_text": "新建數據集", + "please_select_evaluation_dataset": "請選擇評測數據集", + "join_knowledge_base": "加入知識庫", + "all_data_with_count": "全部數據({{num}})", + "question_data_with_count": "問題數據({{num}})", + "error_data_with_count": "異常數據({{num}})", + "export_data": "導出", + "retry_action": "重試", + "basic_info": "基本資訊", + "application": "應用", + "version": "版本", + "evaluation_dataset_name": "評測數據集", + "start_time": "開始時間", + "end_time": "結束時間", + "executor_name": "執行人", + "app_with_search_and_chat_recommendation": "評測應用包含知識庫搜索和AI對話環節,已推薦使用 3 個維度進行評估", + "app_with_chat_recommendation": "評測應用包含AI對話環節,已推薦使用 1 個維度進行評估", + "app_with_search_recommendation": "評測應用包含知識庫搜索環節,已推薦使用 2 個維度進行評估", + "no_dimensions_added": "還沒有添加評測維度,", + "click_to_add": "點擊添加", + "meets_expectation": "符合預期!", + "below_expectation": "低於預期分數!", + "summary_generation_error": "總結內容生成異常,", + "error_message_prefix": "報錯信息:", + "summary_pending_generation": "總結內容待生成", + "summary_generating_content": "總結內容生成中", + "data_with_count": "數據({{data}})", + "search_placeholder": "搜索", + "detail_title": "詳情", + "modify_dataset_simultaneously": "同時修改評測數據集", + "retry_button": "重試", + "edit_action": "編輯", + "delete_action": "刪除", + "confirm_delete_data_in_task": "確認在當前任務中刪除該數據?", + "view_full_response": "查看完整響應", + "abnormal_status": "異常", + "question_field": "問題", + "reference_answer_field": "參考答案", + "actual_answer_field": "實際回答", + "no_answer_available": "暫無回答", + "comprehensive_score_title": "綜合評分", + "dimension_score_title": "維度評分", + "error_data_calculation_notice": "{{count}} 條數據執行異常,僅使用執行成功的數據來計算分數。", + "regenerate_summary_content": "重新生成總結內容", + "processing_status": "處理中...", + "view_results_after_completion": "完成後可查看評測結果", + "all_data_execution_error": "全部數據執行異常,", + "check_error_details": "請查看異常數據中的詳細原因,", + "click_to_retry": "點擊重試", + "comprehensive_score_weight_description": "按照指定權重計算測試數據全部維度的綜合評分,可根據應用使用場景所關注的維度進行設置。", + "no_data_available": "暫無數據", + "question_column": "問題", + "comprehensive_score_column": "綜合評分", + "error_info_column": "異常信息", + "request_failed": "請求失敗", + "retry_request_submitted": "重試請求已提交", + "retry_failed": "重試失敗", + "save_success": "保存成功", + "save_failed": "保存失敗", + "summary_generation_request_submitted": "總結生成請求已提交", + "generate_summary_failed": "生成總結失敗", + "export_failed": "導出失敗", + "load_failed": "加載失敗", + "export_success": "導出成功", + "no_dimension_data_cannot_generate_summary": "暫無維度數據,無法生成總結", + "detail": "詳情", + "eval_file_check_error": "評測文件校驗失敗", + "stauts": "狀態", + "task_name": "任務名稱", + "Task_name": "任務名", + "click_to_download_template": "點擊下載該應用的 CSV 模板", + "evaluation_created": "評測任務創建成功" +} \ No newline at end of file diff --git a/packages/web/i18n/zh-Hant/database_client.json b/packages/web/i18n/zh-Hant/database_client.json new file mode 100644 index 000000000000..9f7c03f3f5f5 --- /dev/null +++ b/packages/web/i18n/zh-Hant/database_client.json @@ -0,0 +1,24 @@ +{ + "client_destory_error": "資料庫連線客戶端銷毀失敗", + "client_not_found": "資料庫連線客戶端未正確初始化", + "not_support_databaseType": "不支援的資料庫類型", + "not_implemented_databaseType": "未實作的資料庫客戶端", + "connection_failed": "資料庫連線失敗,請檢查連線配置", + "authentication_failed": "資料庫認證失敗,請檢查使用者名稱和密碼以及資料庫權限是否正確開放", + "database_not_exist": "資料庫不存在,請檢查資料庫名稱是否正確", + "database_port_error": "資料庫連接埠錯誤,請檢查連接埠是否正確", + "connection_address_error": "連接位址錯誤,請檢查位址是否正確", + "connection_timeout": "資料庫連線逾時,請檢查網路是否通暢", + "connection_refused": "資料庫連線被拒絕,請檢查資料庫是否允許外部連線", + "connection_check_error": "連線測試失敗,請檢查網路是否通暢", + "connection_lost": "資料庫連線中斷,請檢查網路是否通暢", + "host_error": "連線位址錯誤,請檢查位址是否正確", + "invalid_table_name": "無效的表名", + "fetch_info_error": "取得資料庫資訊失敗", + "database_config_not_found": "未配置資料庫連線訊息,請先設定", + "table_not_found": "取得表格資訊失敗,表格不存在", + "illeagal_table_info": "非法的表格訊息,請檢查表格資訊是否正確", + "op_unknown_database_error": "嘗試操作您的資料庫時發生未知錯誤,請檢查資料庫連線是否正常", + "dative_service_error": "連線Dative服務時發生錯誤,請檢查設定是否正確", + "tableNamesDuplicate": "存在多個重複表名" +} \ No newline at end of file diff --git a/packages/web/i18n/zh-Hant/dataset.json b/packages/web/i18n/zh-Hant/dataset.json index 03ae503c5754..3d8c91d7b8d7 100644 --- a/packages/web/i18n/zh-Hant/dataset.json +++ b/packages/web/i18n/zh-Hant/dataset.json @@ -224,79 +224,131 @@ "yuque_dataset_config": "設定語雀知識庫", "yuque_dataset_desc": "可透過設定語雀文件權限,使用語雀文件建構知識庫,文件不會進行二次儲存", "enterprise_database": "資料庫", -"enterprise_database_desc": "授權 MySQL 資料庫後讀取選定的資料表,檢索時連接資料庫獲取相關資訊", -"enterprise_database_embedding_model_tip": "索引模型可將資料庫關鍵資訊(截取部分資料的表名、表描述、列名、列描述)轉成向量,用於進行語義檢索", -"database_structure_change_tip": "資料結構發生變化時,如資料表名稱變更或其中的列名變更等,請手動重新整理資料來源。", -"refresh_data_source": "重新整理資料來源", -"search_name_or_description": "搜尋名稱或描述", -"connect_database": "連接資料庫", -"data_config": "資料配置", -"database_type": "資料庫類型", -"mysql_description": "支援 5.7 及其以上版本", -"database_host": "資料庫位址", -"host_placeholder": "請輸入IP位址或域名", -"host_tips": "請確保資料庫所在與本平台連通,填入可存取的位址", -"host_required": "資料庫位址不能為空", -"port": "連接埠號", -"port_required": "連接埠號不能為空", -"port_invalid": "連接埠號格式不正確", -"port_range_error": "連接埠號必須在1-65535範圍內", -"database_name": "資料庫名稱", -"database_name_placeholder": "請輸入資料庫名稱", -"database_name_required": "資料庫名稱不能為空", -"database_username": "資料庫使用者名稱", -"username_placeholder": "請輸入使用者名稱", -"username_required": "使用者名稱不能為空", -"database_password": "資料庫密碼", -"password_placeholder": "請輸入密碼", -"password_required": "密碼不能為空", -"connection_pool_size": "連接池大小", -"connection_pool_required": "連接池大小不能為空", -"connection_pool_min_error": "連接池大小不能小於1", -"connection_pool_max_error": "連接池大小不能大於100", -"connect_next_step": "連接並下一步", -"database_config_title": "知識庫將呼叫已選資料表中的資料進行索引", -"search_tables": "資料表名", -"table_selection_warning": "存在未選擇內容", -"table_description": "資料表描述", -"table_description_placeholder": "預設值是資料表表自帶的描述", -"default_table_description": "預設值是資料表表自帶的描述", -"column_configuration": "資料列配置", -"search_columns": "列名", -"column_name": "列名", -"column_type": "類型", -"column_description": "描述", -"column_enabled": "啟用", -"default_column_description": "預設值是資料表表自帶的描述", -"confirm": "確認", -"edit_database_config_warning": "資料來源已配置,發生 {{changedCount}} 個資料表存在列的變更,{{deletedCount}} 個資料表已不存在,請修正最新資料。", -"edit_database_warning": "修改資料庫資訊後,當前正在存取資料庫的應用將斷開重連,可能會導致正在執行的應用無法檢索到結果。", -"database_config": "配置", -"refresh_success": "重新整理成功", -"no_data_changes": "未出現資訊變更", -"refresh_datasource": "重新整理資料來源", -"reconnect_success": "重新連接資料庫,變更配置成功", -"auth_failed": "資料庫連接失敗,認證未通過,請檢查使用者名稱和密碼。", -"connection_failed": "連接失敗", -"reconnecting": "重新連接中", -"reconnect_success_detail": "已重新連接資料庫,變更配置成功", -"table_changes_notice": "發現 {{changedCount}} 個資料表存在列的變更,{{deletedCount}} 個資料表已不存在,請核查最新資料。", -"reconnect_database": "重新連接資料庫", -"column_desc_accuracy_tip": "為了提高問答準確率,請準確填寫列描述,用來解釋此列資料的含義和用途,大模型將會根據列描述選擇對應的列資料進行檢索和生成回答", -"default_table_desc_tip": "預設使用資料表中定義的描述。", -"column_enabled_tip": "啟用後表示使用該列資料進行檢索及回答", -"connecting": "正在連接", -"test_connectivity": "測試連通性", -"connect_and_next": "連接並進行下一步", -"connection_network_error": "連接失敗,請檢查網路連接", -"validate_ip_tip": "該輸入項禁止使用本地回環位址,如:localhost、127.x.x.x、0.0.0.0", -"database": "資料庫", -"search_model": "檢索模型", -"search_model_desc": "用於生成可在資料庫中檢索的SQL語句,並進行檢索與匯總,生成可用於對話的文字。", -"search_model_tip": "使用非推理模型、參數量大的模型效果更佳。", -"other_knowledge_base": "其他知識庫", -"database_search": "資料庫檢索", -"description": "描述", -"remove": "移除", -"confirm_remove_database_table": "確認移除該數據表及其所有數據配置?" + "enterprise_database_desc": "授權 MySQL 資料庫後讀取選定的資料表,檢索時連接資料庫獲取相關資訊", + "enterprise_database_embedding_model_tip": "索引模型可將資料庫關鍵資訊(截取部分資料的表名、表描述、列名、列描述)轉成向量,用於進行語義檢索", + "database_structure_change_tip": "資料結構發生變化時,如資料表名稱變更或其中的列名變更等,請手動重新整理資料來源。", + "refresh_data_source": "重新整理資料來源", + "search_name_or_description": "搜尋名稱或描述", + "connect_database": "連接資料庫", + "data_config": "資料配置", + "database_type": "資料庫類型", + "mysql_description": "支援 5.7 及其以上版本", + "database_host": "資料庫位址", + "host_placeholder": "請輸入IP位址或域名", + "host_tips": "請確保資料庫所在與本平台連通,填入可存取的位址", + "host_required": "資料庫位址不能為空", + "port": "連接埠號", + "port_required": "連接埠號不能為空", + "port_invalid": "連接埠號格式不正確", + "port_range_error": "連接埠號必須在1-65535範圍內", + "database_name": "資料庫名稱", + "database_name_placeholder": "請輸入資料庫名稱", + "database_name_required": "資料庫名稱不能為空", + "database_username": "資料庫使用者名稱", + "username_placeholder": "請輸入使用者名稱", + "username_required": "使用者名稱不能為空", + "database_password": "資料庫密碼", + "password_placeholder": "請輸入密碼", + "password_required": "密碼不能為空", + "connection_pool_size": "連接池大小", + "connection_pool_required": "連接池大小不能為空", + "connection_pool_min_error": "連接池大小不能小於1", + "connection_pool_max_error": "連接池大小不能大於100", + "connect_next_step": "連接並下一步", + "database_config_title": "知識庫將呼叫已選資料表中的資料進行索引", + "search_tables": "資料表名", + "table_selection_warning": "存在未選擇內容", + "table_description": "資料表描述", + "table_description_placeholder": "預設值是資料表表自帶的描述", + "default_table_description": "預設值是資料表表自帶的描述", + "column_configuration": "資料列配置", + "search_columns": "列名", + "column_name": "列名", + "column_type": "類型", + "column_description": "描述", + "column_enabled": "啟用", + "default_column_description": "預設值是資料表表自帶的描述", + "confirm": "確認", + "edit_database_config_warning": "資料來源已配置,發生 {{changedCount}} 個資料表存在列的變更,{{deletedCount}} 個資料表已不存在,請修正最新資料。", + "edit_database_warning": "修改資料庫資訊後,當前正在存取資料庫的應用將斷開重連,可能會導致正在執行的應用無法檢索到結果。", + "database_config": "配置", + "refresh_success": "重新整理成功", + "no_data_changes": "未出現資訊變更", + "refresh_datasource": "重新整理資料來源", + "reconnect_success": "重新連接資料庫,變更配置成功", + "auth_failed": "資料庫連接失敗,認證未通過,請檢查使用者名稱和密碼。", + "connection_failed": "連接失敗", + "reconnecting": "重新連接中", + "reconnect_success_detail": "已重新連接資料庫,變更配置成功", + "table_changes_notice": "發現 {{changedCount}} 個資料表存在列的變更,{{deletedCount}} 個資料表已不存在,請核查最新資料。", + "reconnect_database": "重新連接資料庫", + "column_desc_accuracy_tip": "為了提高問答準確率,請準確填寫列描述,用來解釋此列資料的含義和用途,大模型將會根據列描述選擇對應的列資料進行檢索和生成回答", + "default_table_desc_tip": "預設使用資料表中定義的描述。", + "column_enabled_tip": "啟用後表示使用該列資料進行檢索及回答", + "connecting": "正在連接", + "test_connectivity": "測試連通性", + "connect_and_next": "連接並下一步", + "connection_network_error": "連接失敗,請檢查網路連接", + "validate_ip_tip": "該輸入項禁止使用本地回環位址,如:localhost、127.x.x.x、0.0.0.0", + "database": "資料庫", + "search_model": "檢索模型", + "search_model_desc": "用於生成可在資料庫中檢索的SQL語句,並進行檢索與匯總,生成可用於對話的文字。", + "search_model_tip": "使用非推理模型、參數量大的模型效果更佳。", + "other_knowledge_base": "其他知識庫", + "database_search": "資料庫檢索", + "description": "描述", + "remove": "移除", + "confirm_remove_database_table": "確認移除該數據表及其所有數據配置?", + "data_source_refreshed": "資料來源已刷新,", + "found": "發現", + "tables_with_column_changes": "發現 {{modifiedTablesCount}} 個資料表存在欄位的變更,", + "tables_not_exist": "發現 {{delTablesCount}} 個資料表已不存在,", + "please_check_latest_data": "請核查最新資料。", + "has_unfilled_content": "存在未填寫的內容", + "has_column_changes": "存在欄位的變更", + "connection_success": "連接成功", + "no_changes_detected": "未出現信息變更。", + "tables_added": "新增 {{addedTables}} 個數據表", + "tables_modified": "發現 {{modifiedTables}} 個數據表存在列的變更,", + "tables_deleted": "發現 {{deletedTables}} 個數據表已不存在,", + "please_verify_data": "請核查最新數據。", + "changes_detected": "發現", + "new_columns_added_disabled": "個新增列(默認未啟用),", + "columns_no_longer_exist": "個列已不存在,", + "check_latest_data": "請核查最新數據。", + "config": "配置", + "refresh_failed": "刷新失敗", + "unknown_error": "未知錯誤", + "no_data_available": "暫無數據", + "database_sql_query": "資料庫檢索的 SQL 語句", + "search_result": "檢索結果", + "no_search_result": "暫無檢索結果", + "database_address_required": "資料庫地址不能為空", + "ip_format_invalid_range": "IP地址格式不正確,每段數值應在0-255之間", + "database_address_format_invalid": "資料庫地址格式不正確,請輸入有效的IP地址或域名", + "data_index_column_value":"SQL欄位範例值索引", + "error_create_datasetcollection": "建立資料集失敗,請聯絡管理員", + "tables_modified_and_deleted": "發現 {{modifiedTables}} 個數據表存在列的變更,{{deletedTables}} 個數據表已不存在,", + "tables_no_longer_exist_comma": "個數據表已不存在,", + "process.databaseSchema": "資料庫結構索引產生", + "database_dataset_desc": "直連企業內部MySQL、SQLite等資料庫構建知識庫", + "database_connection_config": "資料庫連接配置", + "database_url": "資料庫地址", + "database_url_placeholder": "例如: localhost 或 192.168.1.100", + "database_url_required": "資料庫地址不能為空", + "database_port": "連接埠", + "database_username_placeholder": "資料庫使用者名稱", + "database_username_required": "使用者名稱不能為空", + "database_password_placeholder": "資料庫密碼", + "database_password_required": "密碼不能為空", + "database_max_connections": "最大連接數", + "database_config_description": "配置資料庫連接資訊,系統將直連您的資料庫進行資料查詢和向量化處理。", + "database_required_fields": "請填寫所有必填欄位", + "set_database_config": "配置資料庫連接", + "save_database_config": "儲存配置", + "confirm_update_database_config": "確認更新資料庫配置?", + "confirm_create_database_config": "確認創建資料庫配置?", + "database_config_updated": "資料庫配置已更新", + "data_index_column_description": "SQL欄位描述索引", + "no_data_in_database": "該數據庫中沒有數據" } diff --git a/packages/web/i18n/zh-Hant/evaluation.json b/packages/web/i18n/zh-Hant/evaluation.json index 6a17ac1b2a63..9daeb06c5123 100644 --- a/packages/web/i18n/zh-Hant/evaluation.json +++ b/packages/web/i18n/zh-Hant/evaluation.json @@ -4,7 +4,6 @@ "name_required": "名稱必填", "name_too_long": "名稱過長", "description_too_long": "描述過長", - "dataset_id_required": "數據集ID必填", "target_required": "評估目標必填", "target_invalid_config": "評估目標配置無效", "target_app_id_missing": "應用ID缺失", @@ -27,10 +26,8 @@ "invalid_status": "無效的狀態", "invalid_state_transition": "無效的狀態轉換, 只有排隊狀態或手動停止的評估可以啟動", "only_running_can_stop": "只有運行中的評估可以停止", - "only_failed_can_retry": "只有失敗的評估項可以重試", "item_no_error_to_retry": "評估項無錯誤可重試", - "target_output_required": "目標輸出必填", - "evaluator_output_required": "評估器輸出必填", + "target_execution_error": "評估目標執行錯誤", "dataset_load_failed": "數據集加載失敗", "target_config_invalid": "目標配置無效", "evaluators_config_invalid": "評估器配置無效", @@ -39,7 +36,9 @@ "duplicate_dataset_name": "數據集名稱重複", "no_data_in_collections": "集合中無數據", "update_failed": "更新失敗", - "lock_acquisition_failed": "鎖獲取失敗", + "task_system_error": "處理評估任務時發生系統錯誤", + "manually_stopped": "手動停止", + "evaluator_execution_errors": "評估器執行錯誤", "metric_not_found": "評估指標未找到", "metric_un_auth": "無權操作該評估指標", @@ -50,6 +49,7 @@ "metric_prompt_too_long": "指標提示詞不能超過4000個字符", "metric_type_required": "指標類型不能為空", "metric_type_invalid": "指標類型無效", + "metric_name_invalid": "指標名稱無效", "metric_builtin_cannot_modify": "內建指標不能修改", "metric_builtin_cannot_delete": "內建指標不能刪除", "metric_id_required": "指標ID不能為空", @@ -76,7 +76,7 @@ "evaluator_service_unavailable": "評估器服務不可用", "evaluator_invalid_response": "評估器返回無效響應", "evaluator_network_error": "評估器網絡連接錯誤", - + "eval_id_required": "評估任務ID不能為空", "summary_metrics_config_error": "指標配置錯誤", "summary_threshold_value_required": "閾值不能為空", @@ -113,10 +113,10 @@ "dataset_task_operation_failed": "任務操作失敗", "dataset_task_delete_failed": "數據集任務刪除失敗", "fetch_failed_tasks_error": "獲取失敗任務列表失敗", + "item_job_not_found": "未找到評估項作業", "file_id_required": "文件 ID 是必需的", "file_must_be_csv": "文件必須是 CSV 文件", "csv_invalid_structure": "CSV 文件結構無效", - "csv_too_many_rows": "CSV 文件包含的行數過多(最多 10,000 行)", "csv_parsing_error": "CSV 解析錯誤", "csv_no_data_rows": "CSV 文件不包含數據行", "count_must_be_greater_than_zero": "計數必須大於零", diff --git a/packages/web/package.json b/packages/web/package.json index 4be2dbf6bf4f..b1b07b8b5853 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -1,6 +1,10 @@ { "name": "@fastgpt/web", "version": "1.0.0", + "scripts": { + "check-i18n": "node ../../scripts/i18n/checkI18nCompleteness.js", + "fix-i18n": "node ../../scripts/i18n/checkI18nCompleteness.js --fix" + }, "dependencies": { "@chakra-ui/anatomy": "2.2.1", "@chakra-ui/icons": "2.1.1", diff --git a/packages/web/support/user/audit/constants.ts b/packages/web/support/user/audit/constants.ts index 980e1894ca89..389760aa7b32 100644 --- a/packages/web/support/user/audit/constants.ts +++ b/packages/web/support/user/audit/constants.ts @@ -539,7 +539,7 @@ export const auditLogMap = { params: {} as { name?: string; taskName: string; - datasetId: string; + evalDatasetCollectionId: string; targetType: string; evaluatorCount: number; } @@ -654,7 +654,7 @@ export const auditLogMap = { params: {} as { name?: string; metricName: string; - } + } }, //Evaluation Task DataItem Aggregation [AuditEventEnum.DELETE_EVALUATION_TASK_DATA_ITEM]: { diff --git a/plugins/diting/Dockerfile b/plugins/diting/Dockerfile index aae2e2e1bec7..5e716fac5418 100644 --- a/plugins/diting/Dockerfile +++ b/plugins/diting/Dockerfile @@ -1,5 +1,5 @@ -FROM mirrors.sangfor.com/python:3.11-slim-bookworm AS builder -COPY --from=mirrors.sangfor.com/astral-sh/uv:0.7.17 /uv /uvx /bin/ +FROM python:3.11-slim-bookworm AS builder +COPY --from=astral-sh/uv:0.7.17 /uv /uvx /bin/ WORKDIR /app # Build context ../../ @@ -17,7 +17,12 @@ RUN --mount=type=cache,target=/root/.cache/uv \ --mount=type=bind,source=pyproject.toml,target=pyproject.toml \ uv sync --frozen --package diting-server --no-editable --no-dev -FROM mirrors.sangfor.com/python:3.11-slim-bookworm +FROM python:3.11-slim-bookworm +RUN apt-get update \ + && apt-get install -y curl vim \ + && apt-get clean \ + && rm -rf /var/lib/apt/lists/* + # Copy the environment, but not the source code COPY --from=builder --chown=diting:diting /app/.venv /app/.venv # ENV DT_SERVER_DATA="/app/data" diff --git a/plugins/diting/packages/diting-core/src/diting_core/metrics/answer_correctness/template.py b/plugins/diting/packages/diting-core/src/diting_core/metrics/answer_correctness/template.py index a50c22811e5b..4dc070592d68 100644 --- a/plugins/diting/packages/diting-core/src/diting_core/metrics/answer_correctness/template.py +++ b/plugins/diting/packages/diting-core/src/diting_core/metrics/answer_correctness/template.py @@ -9,6 +9,8 @@ class AnswerCorrectnessTemplate: @staticmethod def generate_statements(user_input: str, text: str) -> str: return f"""Given a question and an answer, analyze the complexity of each sentence in the answer. Break down each sentence into one or more fully understandable statements. Ensure that no pronouns are used in any statement. Format the outputs in JSON. + +IMPORTANT: Generate statements in the SAME LANGUAGE as the input text. If the input is in Chinese, generate Chinese statements. If the input is in English, generate English statements. Do not translate or change the language. Maintain the original language throughout your response. Please return the output in a JSON format that complies with the following schema as specified in JSON Schema: {json.dumps(Statements.model_json_schema())} Do not use single quotes in your response but double quotes, properly escaped with a backslash. @@ -43,6 +45,8 @@ def generate_verdicts( expected_output_statements: list[str], ) -> str: return f"""Given a ground truth and an answer statements, analyze each statement and classify them in one of the following categories: TP (true positive): statements that are present in answer that are also directly supported by the one or more statements in ground truth, FP (false positive): statements present in the answer but not directly supported by any statement in ground truth, FN (false negative): statements found in the ground truth but not present in answer. Each statement can only belong to one of the categories. Provide a reason for each classification. + +IMPORTANT: When providing reasons, use the SAME LANGUAGE as the input. If the input is in Chinese, provide reasons in Chinese. If the input is in English, provide reasons in English. Please return the output in a JSON format that complies with the following schema as specified in JSON Schema: {json.dumps(Verdicts.model_json_schema())} Do not use single quotes in your response but double quotes, properly escaped with a backslash. @@ -147,8 +151,10 @@ def generate_reasons( - **Correctly Included (TP)**: Statements in the response that are factually accurate and directly supported by the ground truth. - **Incorrectly Added (FP)**: Statements in the response that are not supported by the ground truth. - **Missing (FN)**: Important facts present in the ground truth but absent from the response. -These categories are for analysis only. When generating your explanation, do NOT use the terms "TP", "FP", "FN", "true positive", -or any technical evaluation jargon. Provide a concise and user-friendly reason for the score using plain, natural language. +These categories are for analysis only. When generating your explanation, do NOT use the terms "TP", "FP", "FN", "true positive", +or any technical evaluation jargon. Do NOT include the numeric score in your reason. Provide a concise and user-friendly reason using plain, natural language. + +IMPORTANT: Provide your reason in the SAME LANGUAGE as the input. If the input is in Chinese, respond in Chinese. If the input is in English, respond in English. Please return the output in a JSON format that complies with the following schema as specified in JSON Schema: {json.dumps(Reason.model_json_schema())} @@ -156,7 +162,7 @@ def generate_reasons( Example JSON: {{ - "reason": "The score is because because ." + "reason": "" }} If the score is 1, keep it short and say something positive with an upbeat encouraging tone (but don't overdo it). diff --git a/plugins/diting/packages/diting-core/src/diting_core/metrics/answer_relevancy/template.py b/plugins/diting/packages/diting-core/src/diting_core/metrics/answer_relevancy/template.py index 5587e430aeca..dadd8c989568 100644 --- a/plugins/diting/packages/diting-core/src/diting_core/metrics/answer_relevancy/template.py +++ b/plugins/diting/packages/diting-core/src/diting_core/metrics/answer_relevancy/template.py @@ -6,6 +6,8 @@ class AnswerRelevancyTemplate: def generate_statements(actual_output: str): return f"""Given the text, breakdown and generate a list of statements presented. Ambiguous statements and single words can also be considered as statements. +IMPORTANT: Generate statements in the SAME LANGUAGE as the input text. If the input is in Chinese, generate Chinese statements. If the input is in English, generate English statements. Do not translate or change the language. Maintain the original language throughout your response. + Example: Example text: Our new laptop model features a high-resolution Retina display for crystal-clear visuals. It also includes a fast-charging battery, giving you up to 12 hours of usage on a single charge. For security, we’ve added fingerprint authentication and an encrypted SSD. Plus, every purchase comes with a one-year warranty and 24/7 customer support. @@ -35,6 +37,8 @@ def generate_statements(actual_output: str): def generate_verdicts(user_input: str, statements: List[str]): return f"""For the provided list of statements, determine whether each statement is relevant to address the input. Please generate a list of JSON with two keys: `verdict` and `reason`. + +IMPORTANT: When providing reasons, use the SAME LANGUAGE as the input. If the input is in Chinese, provide reasons in Chinese. If the input is in English, provide reasons in English. The 'verdict' key should STRICTLY be either a 'yes', 'idk' or 'no'. Answer 'yes' if the statement is relevant to addressing the original input, 'no' if the statement is irrelevant, and 'idk' if it is ambiguous (eg., not directly relevant but could be used as a supporting point to address the input). The 'reason' is the reason for the verdict. Provide a 'reason' ONLY if the answer is 'no'. @@ -96,7 +100,9 @@ def generate_verdicts(user_input: str, statements: List[str]): @staticmethod def generate_reason(irrelevant_statements: List[str], input: str, score: float): - return f"""Given the answer relevancy score, the list of reasons of irrelevant statements made in the actual output, and the input, provide a CONCISE reason for the score. Explain why it is not higher, but also why it is at its current score. + return f"""Given the answer relevancy score, the list of reasons of irrelevant statements made in the actual output, and the input, provide a CONCISE reason. Do NOT include the numeric score in your reason. Explain the evaluation result based on relevancy. + +IMPORTANT: Provide your reason in the SAME LANGUAGE as the input. If the input is in Chinese, respond in Chinese. If the input is in English, respond in English. The irrelevant statements represent things in the actual output that is irrelevant to addressing whatever is asked/talked about in the input. If there is nothing irrelevant, just say something positive with an upbeat encouraging tone (but don't overdo it otherwise it gets annoying). @@ -105,7 +111,7 @@ def generate_reason(irrelevant_statements: List[str], input: str, score: float): IMPORTANT: Please make sure to only return in JSON format, with the 'reason' key providing the reason. Example JSON: {{ - "reason": "The score is because ." + "reason": "" }} ** diff --git a/plugins/diting/packages/diting-core/src/diting_core/metrics/context_precision/template.py b/plugins/diting/packages/diting-core/src/diting_core/metrics/context_precision/template.py index 43937ce78aac..1a840780031a 100644 --- a/plugins/diting/packages/diting-core/src/diting_core/metrics/context_precision/template.py +++ b/plugins/diting/packages/diting-core/src/diting_core/metrics/context_precision/template.py @@ -9,6 +9,8 @@ class ContextPrecisionTemplate: @staticmethod def generate_verdict(user_input: str, expected_output: str, context: str) -> str: return f"""Given question, answer and context verify if the context was useful in arriving at the given answer. Give verdict as "1" if useful and "0" if not with json output. + +IMPORTANT: When providing reasons, use the SAME LANGUAGE as the input. If the input is in Chinese, provide reasons in Chinese. If the input is in English, provide reasons in English. Please return the output in a JSON format that complies with the following schema as specified in JSON Schema: {json.dumps(Verdict.model_json_schema())} Do not use single quotes in your response but double quotes, properly escaped with a backslash. @@ -58,7 +60,9 @@ def generate_verdict(user_input: str, expected_output: str, context: str) -> str @staticmethod def generate_reason(user_input: str, score: float, verdicts: list[dict[str, Any]]): - return f"""Given the input, retrieval contexts, and contextual precision score, provide a CONCISE summary for the score. Explain why it is not higher, but also why it is at its current score. + return f"""Given the input, retrieval contexts, and contextual precision score, provide a CONCISE summary. Explain the evaluation result but do NOT include the numeric score in your reason. + +IMPORTANT: Provide your reason in the SAME LANGUAGE as the input. If the input is in Chinese, respond in Chinese. If the input is in English, respond in English. The retrieval contexts is a list of JSON with three keys: `verdict`, `reason` (reason for the verdict) and `node`. `verdict` will be either 'yes' or 'no', which represents whether the corresponding 'node' in the retrieval context is relevant to the input. Contextual precision represents if the relevant nodes are ranked higher than irrelevant nodes. Also note that retrieval contexts is given IN THE ORDER OF THEIR RANKINGS. @@ -68,7 +72,7 @@ def generate_reason(user_input: str, score: float, verdicts: list[dict[str, Any] Example JSON: {{ - "reason": "The score is because ." + "reason": "" }} In your reason, you MUST USE the `reason`, QUOTES in the 'reason', and the node RANK (starting from 1, eg. first node) to explain why the 'no' verdicts should be ranked lower than the 'yes' verdicts. diff --git a/plugins/diting/packages/diting-core/src/diting_core/metrics/context_recall/template.py b/plugins/diting/packages/diting-core/src/diting_core/metrics/context_recall/template.py index 6e4b787f503d..8302e313727b 100644 --- a/plugins/diting/packages/diting-core/src/diting_core/metrics/context_recall/template.py +++ b/plugins/diting/packages/diting-core/src/diting_core/metrics/context_recall/template.py @@ -11,6 +11,8 @@ def generate_verdicts( user_input: str, expected_output: str, retrieval_context: List[str] ) -> str: return f"""Given a context, and an answer, analyze each sentence in the answer and classify if the sentence can be attributed to the given context or not. Use only "Yes" (1) or "No" (0) as a binary classification. Output json with reason. + +IMPORTANT: When providing reasons, use the SAME LANGUAGE as the input. If the input is in Chinese, provide reasons in Chinese. If the input is in English, provide reasons in English. Please return the output in a JSON format that complies with the following schema as specified in JSON Schema: {json.dumps(Verdicts.model_json_schema())} Do not use single quotes in your response but double quotes, properly escaped with a backslash. @@ -63,7 +65,9 @@ def generate_reason( unsupportive_reasons: list[str], score: float, ): - return f"""Given the original expected output, a list of supportive reasons, and a list of unsupportive reasons (which are deduced directly from the 'expected output'), and a contextual recall score (closer to 1 the better), summarize a CONCISE reason for the score. + return f"""Given the original expected output, a list of supportive reasons, and a list of unsupportive reasons (which are deduced directly from the 'expected output'), and a contextual recall score (closer to 1 the better), summarize a CONCISE reason. Do NOT include the numeric score in your reason. + +IMPORTANT: Provide your reason in the SAME LANGUAGE as the input. If the input is in Chinese, respond in Chinese. If the input is in English, respond in English. Relate supportive/unsupportive reasons to the sentence number in expected output, and include info regarding the node number in retrieval context to support your final reason. The first mention of "node(s)" should specify "node(s) in retrieval context". Please return the output in a JSON format that complies with the following schema as specified in JSON Schema: @@ -72,7 +76,7 @@ def generate_reason( Example JSON: {{ - "reason": "The score is because ." + "reason": "" }} DO NOT mention 'supportive reasons' and 'unsupportive reasons' in your reason, these terms are just here for you to understand the broader scope of things. diff --git a/plugins/diting/packages/diting-core/src/diting_core/metrics/faithfulness/template.py b/plugins/diting/packages/diting-core/src/diting_core/metrics/faithfulness/template.py index 85f00941de30..2a612ebe36cd 100644 --- a/plugins/diting/packages/diting-core/src/diting_core/metrics/faithfulness/template.py +++ b/plugins/diting/packages/diting-core/src/diting_core/metrics/faithfulness/template.py @@ -9,6 +9,8 @@ class FaithfulnessTemplate: @staticmethod def generate_statements(user_input: str, text: str) -> str: return f"""Given a question and an answer, analyze the complexity of each sentence in the answer. Break down each sentence into one or more fully understandable statements. Ensure that no pronouns are used in any statement. Format the outputs in JSON. + +IMPORTANT: Generate statements in the SAME LANGUAGE as the input text. If the input is in Chinese, generate Chinese statements. If the input is in English, generate English statements. Do not translate or change the language. Maintain the original language throughout your response. Please return the output in a JSON format that complies with the following schema as specified in JSON Schema: {json.dumps(Statements.model_json_schema())} Do not use single quotes in your response but double quotes, properly escaped with a backslash. @@ -42,6 +44,8 @@ def generate_verdicts( statements: List[str], ) -> str: return f"""Your task is to judge the faithfulness of a series of statements based on a given context. For each statement you must return verdict as 1 if the statement can be directly inferred based on the context or 0 if the statement can not be directly inferred based on the context. + +IMPORTANT: When providing reasons, use the SAME LANGUAGE as the input. If the input is in Chinese, provide reasons in Chinese. If the input is in English, provide reasons in English. Please return the output in a JSON format that complies with the following schema as specified in JSON Schema: {json.dumps(Verdicts.model_json_schema())} Do not use single quotes in your response but double quotes,properly escaped with a backslash. @@ -110,7 +114,9 @@ def generate_verdicts( @staticmethod def generate_reason(score: float, contradictions: List[str]): return f"""Below is a list of Contradictions. It is a list of strings explaining why the 'actual output' does not align with the information presented in the 'retrieval context'. Contradictions happen in the 'actual output', NOT the 'retrieval context'. -Given the faithfulness score, which is a 0-1 score indicating how faithful the `actual output` is to the retrieval context (higher the better), CONCISELY summarize the contradictions to justify the score. +Given the faithfulness score, which is a 0-1 score indicating how faithful the `actual output` is to the retrieval context (higher the better), CONCISELY summarize the contradictions. Do NOT include the numeric score in your reason. + +IMPORTANT: Provide your reason in the SAME LANGUAGE as the input. If the input is in Chinese, respond in Chinese. If the input is in English, respond in English. Please return the output in a JSON format that complies with the following schema as specified in JSON Schema: {json.dumps(Reason.model_json_schema())} @@ -118,7 +124,7 @@ def generate_reason(score: float, contradictions: List[str]): Example JSON: {{ - "reason": "The score is because ." + "reason": "" }} If there are no contradictions, just say something positive with an upbeat encouraging tone (but don't overdo it otherwise it gets annoying). diff --git a/projects/app/.env.template b/projects/app/.env.template index 6cc9857e0904..df7bf4b194e6 100644 --- a/projects/app/.env.template +++ b/projects/app/.env.template @@ -100,13 +100,4 @@ CONFIG_JSON_PATH= # Signoz SIGNOZ_BASE_URL= SIGNOZ_SERVICE_NAME= -SIGNOZ_STORE_LEVEL=warn - -# evaluations settings -EVAL_TASK_CONCURRENCY=3 # the number of concurrent evaluations tasks -EVAL_ITEM_CONCURRENCY=10 # the number of concurrent evaluation itmes per evaluations task -EVAL_ITEM_MAX_RETRY=3 # the max retry times for evaluation itmes per evaluations task -EVAL_DATA_QUALITY_CONCURRENCY=2 -EVAL_DATASET_DATA_SYNTHESIZE_CONCURRENCY=5 -EVAL_DATASET_SMART_GENERATE_CONCURRENCY=2 -EVALUATION_DEFAULT_THRESHOLD=80 # the default threshold for evaluation \ No newline at end of file +SIGNOZ_STORE_LEVEL=warn \ No newline at end of file diff --git a/projects/app/data/config.json b/projects/app/data/config.json index 1d0303370ce9..90e81d6fb8ad 100644 --- a/projects/app/data/config.json +++ b/projects/app/data/config.json @@ -16,6 +16,17 @@ "key": "", // 自定义 PDF 解析服务密钥 "doc2xKey": "", // doc2x 服务密钥 "price": 0 // PDF 解析服务价格 + }, + "evalConfig": { + "taskConcurrency": 3, + "caseConcurrency": 10, + "caseMaxRetry": 3, + "caseResultThreshold": 0.8, + "summaryConcurrency": 1, + "dataQualityConcurrency": 2, + "datasetDataSynthesizeConcurrency": 5, + "datasetSmartGenerateConcurrency": 2, + "maxStalledCount": 3 } } } diff --git a/projects/app/data/metric.json b/projects/app/data/metric.json index ec1b7ac5a764..0cfd83abe275 100644 --- a/projects/app/data/metric.json +++ b/projects/app/data/metric.json @@ -2,68 +2,49 @@ "builtinMetrics": [ { "name": "answer_correctness", - "description": "Evaluates the factual accuracy of model responses compared to ground truth answers.", + "description": "Evaluates the factual consistency between the generated answer and the reference answer, evaluating whether it is accurate and error-free.", "userInputRequired": true, "actualOutputRequired": true, "expectedOutputRequired": true, - "contextRequired": false, - "retrievalContextRequired": false, "embeddingRequired": true, "llmRequired": true }, { "name": "answer_similarity", - "description": "Measures semantic similarity between model output and reference answer.", - "userInputRequired": false, + "description": "Evaluates the semantic alignment between the generated answer and the reference answer, determining whether they convey the same core information.", "actualOutputRequired": true, "expectedOutputRequired": true, - "contextRequired": false, - "retrievalContextRequired": false, - "embeddingRequired": true, - "llmRequired": false + "embeddingRequired": true }, { "name": "answer_relevancy", - "description": "Measures how relevant the generated answer is to the user's question.", + "description": "Evaluates how well the generated answer aligns with the question, judging whether the response directly addresses the query.", "userInputRequired": true, - "actualOutputRequired": true, - "expectedOutputRequired": false, - "contextRequired": false, - "retrievalContextRequired": false, - "embeddingRequired": false, + "actualOutputRequired": true, "llmRequired": true }, { "name": "context_precision", - "description": "Measures the proportion of relevant contexts among the retrieved contexts.", + "description": "Evaluates whether high-value information is prioritized in the retrieved content, reflecting the quality of ranking and information density.", "userInputRequired": true, - "actualOutputRequired": false, - "expectedOutputRequired": true, - "contextRequired": false, - "retrievalContextRequired": true, - "embeddingRequired": false, + "expectedOutputRequired": true, + "retrievalContextRequired": true, "llmRequired": true }, { "name": "context_recall", - "description": "Measures the proportion of relevant contexts that were successfully retrieved.", - "userInputRequired": true, - "actualOutputRequired": false, + "description": "Evaluates whether the retrieval system successfully retrieves all key information necessary for formulating the answer, assessing the completeness of retrieval.", + "userInputRequired": true, "expectedOutputRequired": true, - "contextRequired": false, "retrievalContextRequired": true, - "embeddingRequired": false, "llmRequired": true }, { "name": "faithfulness", - "description": "Evaluates whether the generated answer is faithful to the retrieved context.", + "description": "Evaluates whether the generated answer remains faithful to the provided context, determining whether it contains fabricated or inaccurate content.", "userInputRequired": true, "actualOutputRequired": true, - "expectedOutputRequired": false, - "contextRequired": false, "retrievalContextRequired": true, - "embeddingRequired": false, "llmRequired": true } ] diff --git a/projects/app/public/imgs/avatar/summaryAvatar.svg b/projects/app/public/imgs/avatar/summaryAvatar.svg new file mode 100644 index 000000000000..fb361f4c7245 --- /dev/null +++ b/projects/app/public/imgs/avatar/summaryAvatar.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/projects/app/src/components/core/app/DatasetParamsModal.tsx b/projects/app/src/components/core/app/DatasetParamsModal.tsx index 01839a0771fd..32b348b697bc 100644 --- a/projects/app/src/components/core/app/DatasetParamsModal.tsx +++ b/projects/app/src/components/core/app/DatasetParamsModal.tsx @@ -52,6 +52,7 @@ const DatasetParamsModal = ({ maxTokens, hasDatabaseKnowledge = false, hasOtherKnowledge = true, + generateSqlModel = '', onClose, onSuccess }: AppDatasetSearchParamsType & { @@ -90,7 +91,8 @@ const DatasetParamsModal = ({ similarity, datasetSearchUsingExtensionQuery, datasetSearchExtensionModel: datasetSearchExtensionModel || defaultModels.llm?.model, - datasetSearchExtensionBg + datasetSearchExtensionBg, + generateSqlModel: hasDatabaseKnowledge ? generateSqlModel || defaultModels.llm?.model : '' } }); @@ -107,6 +109,7 @@ const DatasetParamsModal = ({ const usingReRankWatch = watch('usingReRank'); const reRankModelWatch = watch('rerankModel'); const rerankWeightWatch = watch('rerankWeight'); + const generateSqlModelWatch = watch('generateSqlModel'); const showSimilarity = useMemo(() => { if (similarity === undefined) return false; @@ -151,27 +154,36 @@ const DatasetParamsModal = ({ - {t('dataset:database')} + {t('common:core.app.workflow.search_knowledge.database')} )} {hasDatabaseKnowledge && ( - {t('dataset:search_model')} + {t('common:search_model')} - {t('dataset:search_model_desc')} + {t('common:search_model_desc')}
- {t('dataset:search_model_tip')} + {t('common:search_model_tip')} } />
- + { + setValue('generateSqlModel', e); + }} + />
)} @@ -181,7 +193,7 @@ const DatasetParamsModal = ({ - {t('dataset:other_knowledge_base')} + {t('common:other_knowledge_base')} diff --git a/projects/app/src/components/core/app/DatasetSelectModal.tsx b/projects/app/src/components/core/app/DatasetSelectModal.tsx index 85349956abe0..6a041d7f1a49 100644 --- a/projects/app/src/components/core/app/DatasetSelectModal.tsx +++ b/projects/app/src/components/core/app/DatasetSelectModal.tsx @@ -72,8 +72,10 @@ export const DatasetSelectModal = ({ ); }; - const getDisableTip = () => { - return scene === 'smartGenerate' ? t('app:no_data_for_smart_generate') : ''; + const getDisableTip = (item: DatasetListItemType) => { + return scene === 'smartGenerate' && isDatasetDisabled(item) + ? t('app:no_data_for_smart_generate') + : ''; }; // Cache compatible datasets by vector model to avoid repeated filtering @@ -255,7 +257,7 @@ export const DatasetSelectModal = ({ onClick={(e) => e.stopPropagation()} // Prevent parent click when clicking checkbox > {item.type !== DatasetTypeEnum.folder && ( - + import('@/pageComponents/dashboard/evaluation/dataset/IntelligentGeneration') +); + +interface EvaluationDatasetSelectorProps { + value: string; + onChange: (datasetId: string) => void; +} + +const EvaluationDatasetSelector: React.FC = ({ + value, + onChange +}) => { + const { t } = useTranslation(); + + // 智能生成数据集弹窗控制 + const { + isOpen: isIntelligentModalOpen, + onOpen: onOpenIntelligentModal, + onClose: onCloseIntelligentModal + } = useDisclosure(); + + const { + data: evaluationDatasetList, + loading: isLoadingDatasets, + runAsync: fetchDatasets + } = useRequest2(getEvaluationDatasetListV2); + + useEffect(() => { + fetchDatasets({}); + }, []); + + // 转换评测数据集列表为 MySelect 需要的格式 + const evaluationDatasetSelectList = useMemo(() => { + const data = (evaluationDatasetList?.list || []).map((item: any) => ({ + label: item.name, + value: item._id + })); + + return [{ label: t('dashboard_evaluation:not_join_evaluation_dataset'), value: 'null' }].concat( + data + ); + }, [evaluationDatasetList, t]); + + // 处理创建数据集 + const handleCreateDataset = useCallback( + (type: 'smart' | 'import') => { + if (type === 'smart') { + onOpenIntelligentModal(); + } else { + // 在新标签页打开文件导入页面 + window.open( + '/dashboard/evaluation/dataset/fileImport?scene=evaluationDatasetList', + '_blank' + ); + } + }, + [onOpenIntelligentModal] + ); + + // 智能生成数据集确认回调 + const handleIntelligentGenerationConfirm = useCallback( + (data: any, datasetId?: string) => { + onCloseIntelligentModal(); + fetchDatasets({}); + }, + [onCloseIntelligentModal, fetchDatasets] + ); + + return ( + <> + + + + + {t('dashboard_evaluation:join_evaluation_dataset')} + + + + {t('dashboard_evaluation:create_new_dataset_btn_text')} + + } + menuList={[ + { + children: [ + { + label: ( + + + {t('dashboard_evaluation:smart_generation')} + + ), + onClick: () => handleCreateDataset('smart') + }, + { + label: ( + + + {t('dashboard_evaluation:file_import')} + + ), + onClick: () => handleCreateDataset('import') + } + ] + } + ]} + /> + + + + + {/* 智能生成数据集弹窗 */} + {isIntelligentModalOpen && ( + + )} + + ); +}; + +export default React.memo(EvaluationDatasetSelector); diff --git a/projects/app/src/components/core/chat/ChatContainer/ChatBox/components/QuoteList.tsx b/projects/app/src/components/core/chat/ChatContainer/ChatBox/components/QuoteList.tsx index 4026f860809d..3e801c7261ed 100644 --- a/projects/app/src/components/core/chat/ChatContainer/ChatBox/components/QuoteList.tsx +++ b/projects/app/src/components/core/chat/ChatContainer/ChatBox/components/QuoteList.tsx @@ -12,10 +12,14 @@ import { getQuoteDataList } from '@/web/core/chat/api'; const QuoteList = React.memo(function QuoteList({ chatItemDataId = '', - rawSearch = [] + rawSearch = [], + applicationId, + chatId }: { chatItemDataId?: string; rawSearch: SearchDataResponseItemType[]; + applicationId?: string; + chatId?: string; }) { const theme = useTheme(); const { appId, outLinkAuthData } = useChatStore(); @@ -23,7 +27,7 @@ const QuoteList = React.memo(function QuoteList({ const RawSourceBoxProps = useContextSelector(ChatBoxContext, (v) => ({ chatItemDataId, appId: v.appId, - chatId: v.chatId, + chatId: chatId || v.chatId, // 优先使用外部传入的chatId ...(v.outLinkAuthData || {}) })); const showRawSource = useContextSelector(ChatItemContext, (v) => v.isShowReadRawSource); @@ -39,7 +43,7 @@ const QuoteList = React.memo(function QuoteList({ datasetDataIdList: rawSearch.map((item) => item.id), collectionIdList: [...new Set(rawSearch.map((item) => item.collectionId))], chatItemDataId, - appId, + appId: applicationId || appId, chatId: RawSourceBoxProps.chatId, ...outLinkAuthData }) diff --git a/projects/app/src/components/core/chat/ChatContainer/ChatBox/components/SelectMarkCollection.tsx b/projects/app/src/components/core/chat/ChatContainer/ChatBox/components/SelectMarkCollection.tsx index 57831eee86b0..393f17378f0a 100644 --- a/projects/app/src/components/core/chat/ChatContainer/ChatBox/components/SelectMarkCollection.tsx +++ b/projects/app/src/components/core/chat/ChatContainer/ChatBox/components/SelectMarkCollection.tsx @@ -1,11 +1,14 @@ -import React, { useState, useMemo, useCallback } from 'react'; -import { ModalBody, ModalFooter, Button, Box } from '@chakra-ui/react'; +import React, { useState, useMemo, useCallback, useEffect } from 'react'; +import { ModalBody, ModalFooter, Button, VStack, FormControl, FormLabel } from '@chakra-ui/react'; import { useTranslation } from 'next-i18next'; +import { useRouter } from 'next/router'; import MyModal from '@fastgpt/web/components/common/MyModal'; import dynamic from 'next/dynamic'; import { type AdminFbkType } from '@fastgpt/global/core/chat/type.d'; import FilesCascader from './FilesCascader'; import type { FileSelection } from './FilesCascader'; +import EvaluationDatasetSelector from './EvaluationDatasetSelector'; +import { getEvaluationList } from '@/web/core/evaluation/task'; const InputDataModal = dynamic(() => import('@/pageComponents/dataset/detail/InputDataModal')); @@ -34,6 +37,12 @@ const SelectMarkCollection = ({ onSuccess: (adminFeedback: AdminFbkType) => void; }) => { const { t } = useTranslation(); + const router = useRouter(); + + // 从路由查询参数获取appId + const appId = useMemo(() => { + return router.query.appId as string; + }, [router.query.appId]); // 级联选择器的值 const cascaderValue: FileSelection = useMemo(() => { @@ -59,20 +68,45 @@ const SelectMarkCollection = ({ [adminMarkData, setAdminMarkData] ); + // 评测数据集选择相关 + const [selectedEvaluationDataset, setSelectedEvaluationDataset] = useState(''); + + // 处理评测数据集选择变化 + const handleEvaluationDatasetChange = useCallback((datasetId: string) => { + setSelectedEvaluationDataset(datasetId); + }, []); + // 处理确认按钮点击 const handleConfirm = useCallback(() => { - if (adminMarkData.datasetId && adminMarkData.collectionId) { + if ( + (adminMarkData.datasetId && adminMarkData.collectionId) || + (selectedEvaluationDataset && selectedEvaluationDataset !== 'null') + ) { // 打开输入数据模态框 setShowInputDataModal(true); } - }, [adminMarkData.datasetId, adminMarkData.collectionId]); + }, [adminMarkData.datasetId, adminMarkData.collectionId, selectedEvaluationDataset]); // 控制是否显示输入数据模态框 const [showInputDataModal, setShowInputDataModal] = useState(false); // 检查是否可以选择(需要同时选择了数据集和集合,且不是"不加入知识库") const canConfirm = - adminMarkData.datasetId && adminMarkData.collectionId && !adminMarkData.noKnowledgeBase; + (adminMarkData.datasetId && adminMarkData.collectionId && !adminMarkData.noKnowledgeBase) || + (selectedEvaluationDataset !== 'null' && selectedEvaluationDataset); + + useEffect(() => { + getEvaluationList({ + pageNum: 1, + pageSize: 1, + appId: appId + }).then((res) => { + const item = res?.list?.[0]; + if (item) { + setSelectedEvaluationDataset(item.evalDatasetCollectionId); + } + }); + }, [appId]); return ( <> @@ -80,14 +114,25 @@ const SelectMarkCollection = ({ - - - + + + + + {t('dashboard_evaluation:join_knowledge_base')} + + + + @@ -442,7 +558,7 @@ const EditDataModal: React.FC = ({ variant="outline" onClick={(e) => { e.preventDefault(); // 阻止默认行为 - onClose(); + onClose(hasStatusChange); }} > {t('dashboard_evaluation:cancel')} @@ -477,10 +593,10 @@ const EditDataModal: React.FC = ({ onConfirm={handleConfirm} defaultValues={{ evaluationStatus: - currentEvaluationStatus === EvaluationStatus.HighQuality - ? EvaluationStatus.NeedsImprovement - : EvaluationStatus.HighQuality, - evaluationResult: currentEvaluationResult + currentQualityResult === EvalDatasetDataQualityResultEnum.highQuality + ? EvaluationStatus.HighQuality + : EvaluationStatus.NeedsImprovement, + evaluationResult: currentQualityReason }} /> diff --git a/projects/app/src/pageComponents/dashboard/evaluation/dataset/detail/ManuallyAddModal.tsx b/projects/app/src/pageComponents/dashboard/evaluation/dataset/detail/ManuallyAddModal.tsx index 5e1e5b33a062..a5bf19a9a536 100644 --- a/projects/app/src/pageComponents/dashboard/evaluation/dataset/detail/ManuallyAddModal.tsx +++ b/projects/app/src/pageComponents/dashboard/evaluation/dataset/detail/ManuallyAddModal.tsx @@ -86,21 +86,29 @@ const ManuallyAddModal = ({ // 检查表单是否有效 const isFormValid = useMemo(() => { - return ( - questionValue.trim() !== '' && answerValue.trim() !== '' && evaluationModelValue.trim() !== '' - ); - }, [questionValue, answerValue, evaluationModelValue]); + const basicValid = questionValue.trim() !== '' && answerValue.trim() !== ''; + if (autoEvaluationValue) { + return basicValid && evaluationModelValue.trim() !== ''; + } + return basicValid; + }, [questionValue, answerValue, autoEvaluationValue, evaluationModelValue]); // 处理表单提交 const handleFormSubmit = useCallback( async (data: ManuallyAddForm) => { - await handleAddData({ + const submitData: any = { userInput: data.question, expectedOutput: data.answer, - evaluationModel: data.evaluationModel, enableQualityEvaluation: data.autoEvaluation, collectionId - }); + }; + + // 只有当开启自动评测时才传递评测模型 + if (data.autoEvaluation) { + submitData.evaluationModel = data.evaluationModel; + } + + await handleAddData(submitData); onConfirm?.(data); onClose?.(); reset(); @@ -168,22 +176,24 @@ const ManuallyAddModal = ({ /> - {/* 质量评测模型 */} - - - {t('dashboard_evaluation:quality_eval_model_label')} - - ({ - value: item.model, - label: item.name - }))} - onChange={(value) => setValue('evaluationModel', value)} - placeholder={t('dashboard_evaluation:select_quality_eval_model_placeholder')} - /> - + {/* 质量评测模型 - 仅在开启自动评测时显示 */} + {autoEvaluationValue && ( + + + {t('dashboard_evaluation:quality_eval_model_label')} + + ({ + value: item.model, + label: item.name + }))} + onChange={(value) => setValue('evaluationModel', value)} + placeholder={t('dashboard_evaluation:select_quality_eval_model_placeholder')} + /> + + )} diff --git a/projects/app/src/pageComponents/dashboard/evaluation/dataset/detail/const.ts b/projects/app/src/pageComponents/dashboard/evaluation/dataset/detail/const.ts index d3e9240d862e..91543d35915c 100644 --- a/projects/app/src/pageComponents/dashboard/evaluation/dataset/detail/const.ts +++ b/projects/app/src/pageComponents/dashboard/evaluation/dataset/detail/const.ts @@ -1,14 +1,18 @@ import { i18nT } from '@fastgpt/web/i18n/utils'; +import { + EvalDatasetDataQualityStatusEnum, + EvalDatasetDataQualityResultEnum +} from '@fastgpt/global/core/evaluation/dataset/constants'; -// 评测结果枚举 +// 评测结果枚举(用于前端状态管理和筛选) export enum EvaluationStatus { All = 'all', - HighQuality = 'highQuality', - NeedsImprovement = 'needsOptimization', - Abnormal = 'error', - NotEvaluated = 'unevaluated', - Evaluating = 'evaluating', - Queuing = 'queuing' + HighQuality = EvalDatasetDataQualityResultEnum.highQuality, + NeedsImprovement = EvalDatasetDataQualityResultEnum.needsOptimization, + Abnormal = EvalDatasetDataQualityStatusEnum.error, + NotEvaluated = EvalDatasetDataQualityStatusEnum.unevaluated, + Evaluating = EvalDatasetDataQualityStatusEnum.evaluating, + Queuing = EvalDatasetDataQualityStatusEnum.queuing } // 评测列表状态映射(含任务状态和结果) diff --git a/projects/app/src/pageComponents/dashboard/evaluation/dataset/errorModal.tsx b/projects/app/src/pageComponents/dashboard/evaluation/dataset/errorModal.tsx index de783ef98bbc..683121e0563d 100644 --- a/projects/app/src/pageComponents/dashboard/evaluation/dataset/errorModal.tsx +++ b/projects/app/src/pageComponents/dashboard/evaluation/dataset/errorModal.tsx @@ -122,9 +122,9 @@ const ErrorModal = ({ isOpen, onClose, collectionId }: ErrorModalProps) => { - + - + @@ -149,7 +149,7 @@ const ErrorModal = ({ isOpen, onClose, collectionId }: ErrorModalProps) => { {t(error.dataId)} - {watchedDimensions.map((dimension, index) => ( - + - + - {dimensions.map((dimension) => ( + {filteredDimensions.map((dimension) => ( {
{t('dashboard_evaluation:source_knowledge_base')}{t('dashboard_evaluation:source_knowledge_base')} {t('dashboard_evaluation:source_chunk')}{t('dashboard_evaluation:error_message')}{t('dashboard_evaluation:error_message')} {t('dashboard_evaluation:operations')}
+ { // 获取推荐维度的函数 const getRecommendedDimensions = useCallback( (hasDatasetSearch: boolean, hasChatNode: boolean): Dimension[] => { - // TODO: 根据节点类型返回具体的推荐维度列表 - // 这里需要根据实际的维度数据结构来定义推荐的维度 - if (hasDatasetSearch && hasChatNode) { - // TODO: 返回包含知识库搜索和AI对话的3个推荐维度 - return [ - // 示例结构,需要根据实际维度定义来替换 - // { id: 'dimension1', name: '相关性', type: 'builtin', ... }, - // { id: 'dimension2', name: '准确性', type: 'builtin', ... }, - // { id: 'dimension3', name: '完整性', type: 'builtin', ... } - ]; - } else if (hasChatNode) { - // TODO: 返回AI对话的1个推荐维度 - return [ - // 示例结构,需要根据实际维度定义来替换 - // { id: 'dimension1', name: '回答质量', type: 'builtin', ... } - ]; - } else if (hasDatasetSearch) { - // TODO: 返回知识库搜索的2个推荐维度 - return [ - // 示例结构,需要根据实际维度定义来替换 - // { id: 'dimension1', name: '检索相关性', type: 'builtin', ... }, - // { id: 'dimension2', name: '检索准确性', type: 'builtin', ... } - ]; + const defaultEmbeddingModel = getWebDefaultEmbeddingModel(embeddingModelList)?.model || ''; + const defaultEvaluationModel = + getWebDefaultEvaluationModel(llmModelList.filter((item) => item.useInEvaluation))?.model || + ''; + + const recommendedDimensions: Dimension[] = []; + + // 生成节点指标:answer_correctness(默认选中) + if (hasChatNode) { + const answerCorrectnessInfo = getBuiltinDimensionInfo('answer_correctness'); + recommendedDimensions.push({ + id: getBuiltinDimensionIdFromName('answer_correctness'), + name: answerCorrectnessInfo + ? t(answerCorrectnessInfo.name) + : t('dashboard_evaluation:builtin_answer_correctness_name'), + type: 'builtin', + description: answerCorrectnessInfo + ? t(answerCorrectnessInfo.description) + : t('dashboard_evaluation:builtin_answer_correctness_desc'), + evaluationModel: defaultEvaluationModel, + indexModel: defaultEmbeddingModel, + llmRequired: true, + embeddingRequired: true, + isSelected: true + }); + } + + // 检索节点指标:faithfulness(默认选中),context_recall(默认选中) + if (hasDatasetSearch) { + const faithfulnessInfo = getBuiltinDimensionInfo('faithfulness'); + recommendedDimensions.push({ + id: getBuiltinDimensionIdFromName('faithfulness'), + name: faithfulnessInfo + ? t(faithfulnessInfo.name) + : t('dashboard_evaluation:builtin_faithfulness_name'), + type: 'builtin', + description: faithfulnessInfo + ? t(faithfulnessInfo.description) + : t('dashboard_evaluation:builtin_faithfulness_desc'), + evaluationModel: defaultEvaluationModel, + indexModel: '', + llmRequired: true, + embeddingRequired: false, + isSelected: true + }); + + const contextRecallInfo = getBuiltinDimensionInfo('context_recall'); + recommendedDimensions.push({ + id: getBuiltinDimensionIdFromName('context_recall'), + name: contextRecallInfo + ? t(contextRecallInfo.name) + : t('dashboard_evaluation:builtin_context_recall_name'), + type: 'builtin', + description: contextRecallInfo + ? t(contextRecallInfo.description) + : t('dashboard_evaluation:builtin_context_recall_desc'), + evaluationModel: defaultEvaluationModel, + indexModel: '', + llmRequired: true, + embeddingRequired: false, + isSelected: true + }); } - return []; + + return recommendedDimensions; }, - [] + [embeddingModelList, llmModelList, t] + ); + + // 获取应用最近使用的数据集 + const { runAsync: getLastUsedDataset } = useRequest2( + async (appId: string) => { + if (!appId) return null; + const result = await getEvaluationList({ + pageNum: 1, + pageSize: 1, + appId: appId + }); + return result.list.length > 0 ? result.list[0] : null; + }, + { + manual: true + } ); // 获取应用详情并分析节点类型 @@ -149,7 +215,7 @@ const CreateModal = ({ isOpen, onClose, onSubmit }: CreateModalProps) => { }, { manual: true, - onSuccess: (appDetail) => { + onSuccess: async (appDetail) => { if (!appDetail?.modules) { setRecommendedDimensionText(''); // 清空推荐维度 @@ -172,19 +238,29 @@ const CreateModal = ({ isOpen, onClose, onSubmit }: CreateModalProps) => { if (hasDatasetSearch && hasChatNode) { setRecommendedDimensionText( - t('评测应用包含知识库搜索和AI对话环节,已推荐使用 3 个维度进行评估') + t('dashboard_evaluation:app_with_search_and_chat_recommendation') ); setShouldAutoExpand(false); } else if (hasChatNode) { - setRecommendedDimensionText(t('评测应用包含AI对话环节,已推荐使用 1 个维度进行评估')); + setRecommendedDimensionText(t('dashboard_evaluation:app_with_chat_recommendation')); setShouldAutoExpand(false); } else if (hasDatasetSearch) { - setRecommendedDimensionText(t('评测应用包含知识库搜索环节,已推荐使用 2 个维度进行评估')); + setRecommendedDimensionText(t('dashboard_evaluation:app_with_search_recommendation')); setShouldAutoExpand(false); } else { setRecommendedDimensionText(''); setShouldAutoExpand(true); } + + // 获取并设置该应用最近使用的数据集 + try { + const lastEvaluation = await getLastUsedDataset(appDetail._id); + if (lastEvaluation?.evalDatasetCollectionId) { + setValue('evalDatasetCollectionId', lastEvaluation.evalDatasetCollectionId); + } + } catch (error) { + console.error('Failed to get last used dataset:', error); + } } } ); @@ -205,14 +281,28 @@ const CreateModal = ({ isOpen, onClose, onSubmit }: CreateModalProps) => { }; const evaluators: EvaluatorSchema[] = data.selectedDimensions.map((dimension) => { + // 对于内置维度,使用英文名称和描述 + let metricName = dimension.name; + let metricDescription = dimension.description; + + if (dimension.type === 'builtin') { + const dimensionName = getBuiltinDimensionNameFromId(dimension.id); + const englishInfo = getBuiltinDimensionEnglishInfo(dimensionName); + if (englishInfo) { + metricName = englishInfo.name; + metricDescription = englishInfo.description; + } + } + const metric: EvalMetricSchemaType = { _id: dimension.id, teamId: '', tmbId: '', - name: dimension.name, - description: dimension.description, + name: metricName, + description: metricDescription, type: dimension.type === 'builtin' ? EvalMetricTypeEnum.Builtin : EvalMetricTypeEnum.Custom, + prompt: dimension.prompt, userInputRequired: true, actualOutputRequired: true, expectedOutputRequired: true, @@ -235,7 +325,7 @@ const CreateModal = ({ isOpen, onClose, onSubmit }: CreateModalProps) => { const createRequest: CreateEvaluationRequest = { name: data.name, - datasetId: data.datasetId, + evalDatasetCollectionId: data.evalDatasetCollectionId, target, evaluators }; @@ -292,24 +382,24 @@ const CreateModal = ({ isOpen, onClose, onSubmit }: CreateModalProps) => { // 获取评测数据集列表 const { - ScrollData: DatasetScrollData, - data: datasets, - isLoading: isLoadingDatasets, - refreshList: fetchDatasets - } = useScrollPagination(getEvaluationDatasetList, { - pageSize: 20, - params: { - searchKey: '' - } - }); + data: evaluationDatasetList, + loading: isLoadingDatasets, + runAsync: fetchDatasets + } = useRequest2(getEvaluationDatasetListV2); + + React.useEffect(() => { + fetchDatasets({}); + }, [fetchDatasets]); // 转换数据集数据为下拉选项格式 const datasetOptions = useMemo(() => { - return datasets.map((dataset) => ({ - label: dataset.name, - value: dataset._id - })); - }, [datasets]); + return (evaluationDatasetList?.list || []).map( + (dataset: { _id: string; name: string; createTime: Date }) => ({ + label: dataset.name, + value: dataset._id + }) + ); + }, [evaluationDatasetList]); const selectedDimensions = watchedValues.selectedDimensions || []; @@ -352,12 +442,12 @@ const CreateModal = ({ isOpen, onClose, onSubmit }: CreateModalProps) => { // 智能生成数据集确认回调 const handleIntelligentGenerationConfirm = useCallback( - (data: any, datasetId?: string) => { + (data: any, evalDatasetCollectionId?: string) => { onCloseIntelligentModal(); - fetchDatasets(); + fetchDatasets({}); // 如果返回了数据集ID,自动选择新创建的数据集 - if (datasetId) { - setValue('datasetId', datasetId); + if (evalDatasetCollectionId) { + setValue('evalDatasetCollectionId', evalDatasetCollectionId); } }, [onCloseIntelligentModal, fetchDatasets, setValue] @@ -445,11 +535,10 @@ const CreateModal = ({ isOpen, onClose, onSubmit }: CreateModalProps) => { setValue('datasetId', val)} + onChange={(val) => setValue('evalDatasetCollectionId', val)} isLoading={isLoadingDatasets} - ScrollData={DatasetScrollData} /> { } ]} /> - @@ -605,13 +699,13 @@ const CreateModal = ({ isOpen, onClose, onSubmit }: CreateModalProps) => { iconSize="48px" text={ - {t('还没有添加评测维度,')} + {t('dashboard_evaluation:no_dimensions_added')} - {t('点击添加')} + {t('dashboard_evaluation:click_to_add')} } @@ -634,7 +728,7 @@ const CreateModal = ({ isOpen, onClose, onSubmit }: CreateModalProps) => { isDisabled={ !watchedValues.name || !watchedValues.appId || - !watchedValues.datasetId || + !watchedValues.evalDatasetCollectionId || selectedDimensions.length === 0 } onClick={handleSubmit((data) => createTask(data))} diff --git a/projects/app/src/pageComponents/dashboard/evaluation/task/ManageDimension.tsx b/projects/app/src/pageComponents/dashboard/evaluation/task/ManageDimension.tsx index d38df44e5dbd..164a6a6aa91c 100644 --- a/projects/app/src/pageComponents/dashboard/evaluation/task/ManageDimension.tsx +++ b/projects/app/src/pageComponents/dashboard/evaluation/task/ManageDimension.tsx @@ -16,18 +16,23 @@ import { useForm, useFieldArray } from 'react-hook-form'; import MyIcon from '@fastgpt/web/components/common/Icon'; import MyTag from '@fastgpt/web/components/common/Tag'; import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip'; -import { useScrollPagination } from '@fastgpt/web/hooks/useScrollPagination'; import AIModelSelector from '@/components/Select/AIModelSelector'; import { useSystemStore } from '@/web/common/system/useSystemStore'; import { useMemo } from 'react'; import { getMetricList } from '@/web/core/evaluation/dimension'; import type { EvalMetricDisplayType } from '@fastgpt/global/core/evaluation/metric/type'; -import type { PaginationProps, PaginationResponse } from '@fastgpt/web/common/fetch/type'; import { getWebDefaultEmbeddingModel, getWebDefaultEvaluationModel } from '@/web/common/system/utils'; import EmptyTip from '@fastgpt/web/components/common/EmptyTip'; +import MyBox from '@fastgpt/web/components/common/MyBox'; +import { useRequest2 } from '@fastgpt/web/hooks/useRequest'; +import { + getBuiltinDimensionInfo, + getBuiltinDimensionNameFromId +} from '@/web/core/evaluation/utils'; +import { EvalMetricTypeEnum } from '@fastgpt/global/core/evaluation/metric/constants'; // 维度类型定义 export interface Dimension { @@ -35,10 +40,11 @@ export interface Dimension { name: string; type: 'builtin' | 'custom'; description: string; - evaluationModel: string; // 评测模型 (对应 llmRequired) - indexModel?: string; // 索引模型 (对应 embeddingRequired) - llmRequired: boolean; // 是否需要评测模型 - embeddingRequired: boolean; // 是否需要索引模型 + prompt?: string; + evaluationModel: string; + indexModel?: string; + llmRequired: boolean; + embeddingRequired: boolean; isSelected: boolean; } @@ -49,26 +55,38 @@ interface ManageDimensionProps { onConfirm: (dimensions: Dimension[]) => void; } -// 转换 API 数据为组件所需格式 const transformMetricToDimension = ( metric: EvalMetricDisplayType, defaultEmbeddingModel?: string, - defaultEvaluationModel?: string + defaultEvaluationModel?: string, + t?: any ): Dimension => { + let name = metric.name; + let description = metric.description || ''; + + if (metric.type === EvalMetricTypeEnum.Builtin) { + const dimensionName = getBuiltinDimensionNameFromId(metric._id); + const builtinInfo = getBuiltinDimensionInfo(dimensionName); + if (builtinInfo && t) { + name = t(builtinInfo.name); + description = t(builtinInfo.description); + } + } + return { id: metric._id, - name: metric.name, + name, type: metric.type === 'builtin_metric' ? 'builtin' : 'custom', - description: metric.description || '', - evaluationModel: metric.llmRequired ? defaultEvaluationModel || '' : '', // 使用默认评估模型 - indexModel: metric.embeddingRequired ? defaultEmbeddingModel || '' : undefined, // 如果不需要索引模型则为 undefined - llmRequired: metric.llmRequired, - embeddingRequired: metric.embeddingRequired, + description, + prompt: metric.prompt, + evaluationModel: metric.llmRequired ? defaultEvaluationModel || '' : '', + indexModel: metric.embeddingRequired ? defaultEmbeddingModel || '' : '', + llmRequired: metric.llmRequired ?? false, + embeddingRequired: metric.embeddingRequired ?? false, isSelected: false }; }; -// 维度项组件 const DimensionItem = ({ dimension, isSelected, @@ -134,7 +152,6 @@ const DimensionItem = ({ - {/* 评测模型选择器 */} {dimension.llmRequired && ( )} - {/* 索引模型选择器 */} {dimension.embeddingRequired && ( , - [t] - ); - - // API 适配器函数 - 转换参数格式 - const getMetricListAdapter = useCallback( - async (data: PaginationProps): Promise> => { - return getMetricList({ - pageNum: Math.floor(Number(data.offset) / Number(data.pageSize)) + 1, - pageSize: Number(data.pageSize), - searchKey: '' - }); + // 获取维度列表 + const { + data: metricListData, + loading: isLoading, + runAsync: fetchMetricList + } = useRequest2( + async () => { + const result = await getMetricList({}); + return result; }, - [] + { + manual: false, + refreshDeps: [isOpen], + ready: isOpen + } ); - // 获取维度列表 - 使用滚动分页 - const { - data: metricList, - ScrollData, - refreshList - } = useScrollPagination(getMetricListAdapter, { - pageSize: 10, - params: {}, - refreshDeps: [isOpen], - EmptyTip: EmptyTipDom, - disabled: !isOpen - }); - // 转换并合并维度数据 const transformedDimensions = useMemo(() => { - if (!metricList.length) return []; + if (!metricListData?.list?.length) return []; const defaultEmbeddingModel = getWebDefaultEmbeddingModel(embeddingModelList)?.model; const defaultEvaluationModel = getWebDefaultEvaluationModel(evalModelList)?.model; - return metricList.map((metric) => { + return metricListData.list.map((metric) => { const dimension = transformMetricToDimension( metric, defaultEmbeddingModel, - defaultEvaluationModel + defaultEvaluationModel, + t ); const selectedDimension = selectedDimensions.find((s) => s.id === dimension.id); @@ -278,6 +281,7 @@ const ManageDimension = ({ // 如果在选中列表中,保持选中状态并使用已配置的模型 return { ...dimension, + prompt: selectedDimension.prompt || dimension.prompt, evaluationModel: selectedDimension.evaluationModel || dimension.evaluationModel, indexModel: selectedDimension.indexModel || dimension.indexModel, isSelected: true @@ -285,10 +289,10 @@ const ManageDimension = ({ } return dimension; }); - }, [metricList, selectedDimensions, embeddingModelList, evalModelList]); + }, [metricListData, selectedDimensions, embeddingModelList, evalModelList, t]); // 同步转换后的维度数据到表单,保持已有的选择状态 - useEffect(() => { + const syncDimensions = useCallback(() => { if (transformedDimensions.length > 0) { const currentDimensions = watchedDimensions; @@ -298,33 +302,34 @@ const ManageDimension = ({ return; } - // 合并新数据和已有数据,保持已有的选择状态 - const mergedDimensions = transformedDimensions.map((newDimension) => { + // 检查是否需要更新(避免不必要的更新) + const needsUpdate = transformedDimensions.some((newDimension) => { const existingDimension = currentDimensions.find((d) => d.id === newDimension.id); + return !existingDimension; + }); - if (existingDimension) { - // 如果维度已存在,保持其当前状态(选择状态和模型配置) - return existingDimension; - } + if (needsUpdate) { + // 合并新数据和已有数据,保持已有的选择状态 + const mergedDimensions = transformedDimensions.map((newDimension) => { + const existingDimension = currentDimensions.find((d) => d.id === newDimension.id); - // 新维度,使用转换后的默认状态 - return newDimension; - }); + if (existingDimension) { + // 如果维度已存在,保持其当前状态(选择状态和模型配置) + return existingDimension; + } - // 添加新加载的维度(在滚动加载时) - const existingIds = currentDimensions.map((d) => d.id); - const newDimensions = transformedDimensions.filter((d) => !existingIds.includes(d.id)); + // 新维度,使用转换后的默认状态 + return newDimension; + }); - if (newDimensions.length > 0) { - // 有新维度,追加到现有列表 - const finalDimensions = [...currentDimensions, ...newDimensions]; - replace(finalDimensions); - } else if (mergedDimensions.length !== currentDimensions.length) { - // 维度数量发生变化,更新列表 replace(mergedDimensions); } } - }, [transformedDimensions, replace, watchedDimensions]); + }, [transformedDimensions, watchedDimensions, replace]); + + useEffect(() => { + syncDimensions(); + }, [syncDimensions]); // 处理维度选择 const handleDimensionToggle = useCallback( @@ -359,12 +364,11 @@ const ManageDimension = ({ // 刷新维度列表 const handleRefresh = useCallback(() => { - refreshList(); - }, [refreshList]); + fetchMetricList(); + }, [fetchMetricList]); // 新建维度 const handleCreateDimension = useCallback(() => { - // TODO: window.open('/dashboard/evaluation/dimension/create', '_blank'); }, []); @@ -375,15 +379,10 @@ const ManageDimension = ({ onClose(); }, [watchedDimensions, onConfirm, onClose]); - // 计算真实的选中数量:包括表单中的选中维度和传入的已选维度(可能还未加载到表单中) + // 计算当前选中的维度数量 const selectedCount = useMemo(() => { - const formSelectedIds = watchedDimensions.filter((d) => d.isSelected).map((d) => d.id); - const propsSelectedIds = selectedDimensions.map((d) => d.id); - - // 合并两个数组并去重,得到真实的选中维度数量 - const allSelectedIds = new Set([...formSelectedIds, ...propsSelectedIds]); - return allSelectedIds.size; - }, [watchedDimensions, selectedDimensions]); + return watchedDimensions.filter((d) => d.isSelected).length; + }, [watchedDimensions]); const isMaxSelected = selectedCount >= MAX_SELECTION_COUNT; @@ -411,32 +410,36 @@ const ManageDimension = ({ {t('dashboard_evaluation:create_new_dimension')} - - {fields.map((field, index) => { - const dimension = watchedDimensions[index]; - const isSelected = dimension?.isSelected || false; - const isDisabled = !isSelected && isMaxSelected; - - return ( - - ); - })} - + + {fields.length === 0 && !isLoading ? ( + + ) : ( + fields.map((field, index) => { + const dimension = watchedDimensions[index]; + const isSelected = dimension?.isSelected || false; + const isDisabled = !isSelected && isMaxSelected; + + return ( + + ); + }) + )} + diff --git a/projects/app/src/pageComponents/dashboard/evaluation/task/detail/BasicInfo.tsx b/projects/app/src/pageComponents/dashboard/evaluation/task/detail/BasicInfo.tsx new file mode 100644 index 000000000000..17d74bf8f5ae --- /dev/null +++ b/projects/app/src/pageComponents/dashboard/evaluation/task/detail/BasicInfo.tsx @@ -0,0 +1,141 @@ +import React, { useState, useMemo } from 'react'; +import { Box, Flex, Collapse, HStack, IconButton } from '@chakra-ui/react'; +import { useTranslation } from 'next-i18next'; +import Avatar from '@fastgpt/web/components/common/Avatar'; +import UserBox from '@fastgpt/web/components/common/UserBox'; +import MyIcon from '@fastgpt/web/components/common/Icon'; +import format from 'date-fns/format'; +import type { EvaluationDisplayType } from '@fastgpt/global/core/evaluation/type'; + +interface BasicInfoProps { + evaluationDetail: EvaluationDisplayType | null; +} + +const BasicInfo: React.FC = ({ evaluationDetail }) => { + const { t } = useTranslation(); + const [isExpanded, setIsExpanded] = useState(false); + + const toggleExpanded = () => { + setIsExpanded(!isExpanded); + }; + + // 格式化时间的辅助函数 + const formatTime = useMemo(() => { + return (time: Date | string | undefined) => { + if (!time) return '-'; + try { + if (typeof time === 'string' && time.includes('-') && !time.includes('T')) { + return time; + } + return format(new Date(time), 'yyyy-MM-dd HH:mm:ss'); + } catch (error) { + return '-'; + } + }; + }, []); + + // 安全获取值的辅助函数 + const safeValue = (value: string | undefined | null) => { + return value && value.trim() ? value : '-'; + }; + + return ( + + + + {t('dashboard_evaluation:basic_info')} + + + } + /> + + + {/* 可折叠的内容区域 */} + + + + + {t('dashboard_evaluation:application')} + + + + {safeValue(evaluationDetail?.target?.config?.appName)} + + + + + + {t('dashboard_evaluation:version')} + + + {safeValue(evaluationDetail?.target?.config?.versionName)} + + + + + + {t('dashboard_evaluation:evaluation_dataset_name')} + + {safeValue(evaluationDetail?.evalDatasetCollectionName)} + + + + + {t('dashboard_evaluation:start_time')} + + {formatTime(evaluationDetail?.createTime)} + + + {evaluationDetail?.finishTime && ( + + + {t('dashboard_evaluation:end_time')} + + {formatTime(evaluationDetail.finishTime)} + + )} + + + + {t('dashboard_evaluation:executor_name')} + + {evaluationDetail?.sourceMember ? ( + + ) : ( + - + )} + + + + + ); +}; + +export default BasicInfo; diff --git a/projects/app/src/pageComponents/dashboard/evaluation/task/detail/ConfigParams.tsx b/projects/app/src/pageComponents/dashboard/evaluation/task/detail/ConfigParams.tsx index 7cffc0f35288..689db689bf20 100644 --- a/projects/app/src/pageComponents/dashboard/evaluation/task/detail/ConfigParams.tsx +++ b/projects/app/src/pageComponents/dashboard/evaluation/task/detail/ConfigParams.tsx @@ -1,10 +1,8 @@ -import React, { useState, useCallback, useMemo } from 'react'; +import React, { useCallback, useMemo, useEffect } from 'react'; import { Box, Button, Flex, - Grid, - HStack, ModalBody, ModalFooter, Table, @@ -24,143 +22,107 @@ import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel'; import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip'; import MySelect from '@fastgpt/web/components/common/MySelect'; import MyNumberInput from '@fastgpt/web/components/common/Input/NumberInput'; +import MyBox from '@fastgpt/web/components/common/MyBox'; +import { useRequest2 } from '@fastgpt/web/hooks/useRequest'; +import { getSummaryConfigDetail, postUpdateSummaryConfig } from '@/web/core/evaluation/task'; +import { CalculateMethodEnum, CaculateMethodMap } from '@fastgpt/global/core/evaluation/constants'; +import { getBuiltinDimensionInfo } from '@/web/core/evaluation/utils'; +import type { + UpdateSummaryConfigBody, + UpdateMetricConfigItem +} from '@fastgpt/global/core/evaluation/summary/api'; -// 分数聚合方式枚举 -enum ScoreAggregationType { - Average = 'average', - Median = 'median' -} - -// 评测维度类型 interface EvaluationDimension { - id: string; + metricId: string; name: string; - description: string; // 维度描述 + description: string; threshold: number; weight: number; } -// 表单数据类型 interface ConfigParamsForm { - aggregationType: ScoreAggregationType; + calculateType: CalculateMethodEnum; dimensions: EvaluationDimension[]; } -// TODO: 临时mock数据,待外部传入dimensions时移除 -const mockDimensions: EvaluationDimension[] = [ - { - id: '1', - name: '回答准确度', - description: '评估回答内容的准确性和事实正确性', - threshold: 80, - weight: 30 - }, - { - id: '2', - name: '问题相关度', - description: '评估回答与问题的相关程度', - threshold: 80, - weight: 15 - }, - { - id: '3', - name: '回答创意性', - description: '评估回答的创新性和独特性', - threshold: 80, - weight: 10 - }, - { - id: '4', - name: '回答清晰度', - description: '评估回答的表达清晰度和可理解性', - threshold: 70, - weight: 10 - }, - { - id: '5', - name: '回答完整性', - description: '评估回答是否全面覆盖了问题的各个方面', - threshold: 75, - weight: 10 - }, - { - id: '6', - name: '语言流畅度', - description: '评估回答的语言表达流畅程度', - threshold: 70, - weight: 5 - }, - { - id: '7', - name: '逻辑连贯性', - description: '评估回答的逻辑结构和连贯性', - threshold: 75, - weight: 5 - }, - { - id: '8', - name: '专业术语使用', - description: '评估回答中专业术语使用的准确性和适当性', - threshold: 65, - weight: 5 - }, - { - id: '9', - name: '回答时效性', - description: '评估回答内容的时效性和最新程度', - threshold: 60, - weight: 5 - }, - { - id: '10', - name: '回答友好度', - description: '评估回答的语气和表达是否友好、易于接受', - threshold: 70, - weight: 5 - } -]; - -// 默认表单数据 -const defaultForm: ConfigParamsForm = { - aggregationType: ScoreAggregationType.Average, - dimensions: mockDimensions -}; - -// 分数聚合方式选项 -const aggregationOptions = [ - { value: ScoreAggregationType.Average, label: '平均值' }, - { value: ScoreAggregationType.Median, label: '中位数' } -]; - const ConfigParamsModal = ({ isOpen, onClose, onConfirm, - defaultData = defaultForm, - dimensions // 外部传入的评测维度数据 + evalTaskId }: { isOpen: boolean; onClose: () => void; - onConfirm: (data: ConfigParamsForm) => void; - defaultData?: ConfigParamsForm; - dimensions?: EvaluationDimension[]; // 可选的外部维度数据 + onConfirm?: () => void; + evalTaskId: string; }) => { const { t } = useTranslation(); - // 使用外部传入的dimensions或默认数据 - const formDefaultValues = useMemo(() => { - return { - ...defaultData, - dimensions: dimensions || defaultData.dimensions - }; - }, [defaultData, dimensions]); + // 分数聚合方式选项 + const aggregationOptions = useMemo( + () => [ + { + value: CalculateMethodEnum.mean, + label: t(CaculateMethodMap[CalculateMethodEnum.mean].name) + }, + { + value: CalculateMethodEnum.median, + label: t(CaculateMethodMap[CalculateMethodEnum.median].name) + } + ], + [t] + ); - const { control, handleSubmit, watch, setValue } = useForm({ - defaultValues: formDefaultValues + const { control, handleSubmit, watch, setValue, reset } = useForm({ + defaultValues: { + calculateType: CalculateMethodEnum.mean, + dimensions: [] + } }); const watchedDimensions = watch('dimensions'); + // 加载配置数据 + const { run: loadConfigData, loading: loadingData } = useRequest2( + async () => { + if (!evalTaskId) return; + + const configData = await getSummaryConfigDetail(evalTaskId); + + // 转换数据格式 + const dimensions: EvaluationDimension[] = configData.metricsConfig.map((metric) => { + // 优先使用内置维度的国际化名称和描述 + const builtinInfo = getBuiltinDimensionInfo(metric.metricName); + const displayName = builtinInfo ? t(builtinInfo.name) : metric.metricName; + const description = builtinInfo ? t(builtinInfo.description) : metric.metricDescription; + + return { + metricId: metric.metricId, + name: displayName, + description: description, + threshold: (metric.thresholdValue || 0.8) * 100, // 阈值乘以100转换为百分比显示 + weight: metric.weight + }; + }); + + // 重置表单数据 + reset({ + calculateType: configData.calculateType, + dimensions + }); + }, + { + errorToast: t('common:load_failed') + } + ); + + // 弹窗打开时加载数据 + useEffect(() => { + if (isOpen) { + loadConfigData(); + } + }, [isOpen, loadConfigData]); + // 计算综合评分权重总和 const totalWeight = useMemo(() => { return watchedDimensions.reduce((sum, dimension) => sum + dimension.weight, 0); @@ -232,31 +194,55 @@ const ConfigParamsModal = ({ [watchedDimensions, setValue] ); + // 处理表单提交 + const { run: handleFormSubmit, loading: submitting } = useRequest2( + async (data: ConfigParamsForm) => { + // 转换数据格式 + const metricsConfig: UpdateMetricConfigItem[] = data.dimensions.map((dimension) => ({ + metricId: dimension.metricId, + thresholdValue: dimension.threshold / 100, // 阈值除以100转换为0-1范围 + weight: dimension.weight + })); + + const updateData: UpdateSummaryConfigBody = { + evalId: evalTaskId, + calculateType: data.calculateType, + metricsConfig + }; + + await postUpdateSummaryConfig(updateData); + }, + { + successToast: t('common:save_success'), + errorToast: t('common:save_failed'), + onSuccess: () => { + onConfirm?.(); + onClose(); + } + } + ); + return ( {/* 分数聚合方式 */} - - {t('dashboard_evaluation:score_aggregation_method')} - - + {t('dashboard_evaluation:score_aggregation_method')} (
@@ -418,10 +404,8 @@ const ConfigParamsModal = ({ diff --git a/projects/app/src/pageComponents/dashboard/evaluation/task/detail/DetailedResponseModal.tsx b/projects/app/src/pageComponents/dashboard/evaluation/task/detail/DetailedResponseModal.tsx new file mode 100644 index 000000000000..e9c6ec5a8dc6 --- /dev/null +++ b/projects/app/src/pageComponents/dashboard/evaluation/task/detail/DetailedResponseModal.tsx @@ -0,0 +1,63 @@ +import React from 'react'; +import { useTranslation } from 'next-i18next'; +import MyModal from '@fastgpt/web/components/common/MyModal'; +import EmptyTip from '@fastgpt/web/components/common/EmptyTip'; +import { useRequest2 } from '@fastgpt/web/hooks/useRequest'; +import { getChatResData } from '@/web/core/chat/api'; +import { ResponseBox } from '@/components/core/chat/components/WholeResponseModal'; + +interface DetailedResponseModalProps { + isOpen: boolean; + onClose: () => void; + chatId?: string; + dataId: string; + appId: string; + chatTime?: Date; +} + +const DetailedResponseModal = ({ + isOpen, + onClose, + chatId, + dataId, + appId, + chatTime = new Date() +}: DetailedResponseModalProps) => { + const { t } = useTranslation(); + + const { loading: isLoading, data: response } = useRequest2( + () => getChatResData({ chatId, dataId, appId }), + { + manual: false, + ready: isOpen && !!dataId && !!appId + } + ); + + return ( + + {!!response?.length ? ( + + ) : ( + + )} + + ); +}; + +export default DetailedResponseModal; diff --git a/projects/app/src/pageComponents/dashboard/evaluation/task/detail/EvaluationSummaryCard.tsx b/projects/app/src/pageComponents/dashboard/evaluation/task/detail/EvaluationSummaryCard.tsx new file mode 100644 index 000000000000..b7226dfbf8a1 --- /dev/null +++ b/projects/app/src/pageComponents/dashboard/evaluation/task/detail/EvaluationSummaryCard.tsx @@ -0,0 +1,187 @@ +import React, { useState, useCallback } from 'react'; +import { Box, Flex, Text, IconButton, Link } from '@chakra-ui/react'; +import { useTranslation } from 'next-i18next'; +import MyImage from '@fastgpt/web/components/common/Image/MyImage'; +import MyIcon from '@fastgpt/web/components/common/Icon'; +import GradientBorderBox from './GradientBorderBox'; +import { SummaryStatusEnum } from '@fastgpt/global/core/evaluation/constants'; +import { getBuiltinDimensionInfo } from '@/web/core/evaluation/utils'; +import type { EvaluationSummaryResponse } from '@fastgpt/global/core/evaluation/summary/api'; + +interface EvaluationSummaryCardProps { + data: EvaluationSummaryResponse['data']; + onRetryGeneration?: (metricIds: string[]) => Promise; +} + +const EvaluationSummaryCard: React.FC = ({ + data, + onRetryGeneration +}) => { + const { t } = useTranslation(); + const [currentIndex, setCurrentIndex] = useState(0); + + const handlePrevious = useCallback(() => { + setCurrentIndex((prev) => Math.max(0, prev - 1)); + }, []); + + const handleNext = useCallback(() => { + setCurrentIndex((prev) => Math.min(data.length - 1, prev + 1)); + }, [data.length]); + + const handleRetryGeneration = useCallback( + async (item: EvaluationSummaryResponse['data'][0]) => { + if (!item.metricId || !onRetryGeneration) return; + + await onRetryGeneration([item.metricId]); + }, + [onRetryGeneration] + ); + + const renderSummaryContent = useCallback( + (item: EvaluationSummaryResponse['data'][0]) => { + const { summaryStatus, customSummary, errorReason } = item; + + if (summaryStatus === SummaryStatusEnum.completed) { + return ( + + {customSummary} + + ); + } + + if (summaryStatus === SummaryStatusEnum.failed) { + return ( + + + {t('dashboard_evaluation:summary_generation_error')} + handleRetryGeneration(item)} + ml={1} + > + {t('dashboard_evaluation:click_to_retry')} + + + + {t('dashboard_evaluation:error_message_prefix')} + {errorReason} + + + ); + } + + if ( + summaryStatus === SummaryStatusEnum.pending || + summaryStatus === SummaryStatusEnum.generating + ) { + const statusText = + summaryStatus === SummaryStatusEnum.pending + ? t('dashboard_evaluation:summary_pending_generation') + : t('dashboard_evaluation:summary_generating_content'); + + return ( + + + + {statusText} + + + ); + } + + return null; + }, + [t, handleRetryGeneration] + ); + + const renderSingleDimension = useCallback( + (item: EvaluationSummaryResponse['data'][0], index: number) => { + const dimensionInfo = getBuiltinDimensionInfo(item.metricName); + const dimensionName = t(dimensionInfo?.name) || item.metricName; + const isExpected = item.metricScore >= item.threshold; + const expectationText = isExpected + ? t('dashboard_evaluation:meets_expectation') + : t('dashboard_evaluation:below_expectation'); + const title = `${dimensionName}${expectationText}`; + + return ( + + + + + {title} + + + {renderSummaryContent(item)} + + ); + }, + [t, renderSummaryContent] + ); + + if (data.length === 0) { + return null; + } + + // 单个维度场景 + if (data.length === 1) { + return {renderSingleDimension(data[0], 0)}; + } + + // 两个维度场景 + if (data.length === 2) { + return ( + + {renderSingleDimension(data[0], 0)} + + {renderSingleDimension(data[1], 1)} + + ); + } + + // 三个及以上维度场景 - 轮播模式 + return ( + + {renderSingleDimension(data[currentIndex], currentIndex)} + + + } + size="sm" + variant="ghost" + isDisabled={currentIndex === 0} + onClick={handlePrevious} + color={currentIndex === 0 ? 'myGray.300' : 'myGray.500'} + /> + + + + {currentIndex + 1} + + + /{data.length} + + + + } + size="sm" + variant="ghost" + isDisabled={currentIndex === data.length - 1} + onClick={handleNext} + color={currentIndex === data.length - 1 ? 'myGray.300' : 'myGray.500'} + /> + + + ); +}; + +export default EvaluationSummaryCard; diff --git a/projects/app/src/pageComponents/dashboard/evaluation/task/detail/GradientBorderBox.tsx b/projects/app/src/pageComponents/dashboard/evaluation/task/detail/GradientBorderBox.tsx new file mode 100644 index 000000000000..eacd9ac1b643 --- /dev/null +++ b/projects/app/src/pageComponents/dashboard/evaluation/task/detail/GradientBorderBox.tsx @@ -0,0 +1,40 @@ +import React from 'react'; +import type { BoxProps } from '@chakra-ui/react'; +import { Box } from '@chakra-ui/react'; + +interface GradientBorderBoxProps extends BoxProps { + children: React.ReactNode; +} + +const GradientBorderBox: React.FC = ({ children, ...boxProps }) => { + return ( + + {children} + + ); +}; + +export default GradientBorderBox; diff --git a/projects/app/src/pageComponents/dashboard/evaluation/task/detail/NavBar.tsx b/projects/app/src/pageComponents/dashboard/evaluation/task/detail/NavBar.tsx new file mode 100644 index 000000000000..71352a69e0a0 --- /dev/null +++ b/projects/app/src/pageComponents/dashboard/evaluation/task/detail/NavBar.tsx @@ -0,0 +1,188 @@ +import React, { useCallback, useMemo } from 'react'; +import { useTranslation } from 'next-i18next'; +import { Box, Flex, useTheme, Button, HStack } from '@chakra-ui/react'; +import MyIcon from '@fastgpt/web/components/common/Icon'; +import { useRouter } from 'next/router'; +import { useContextSelector } from 'use-context-selector'; +import { TaskPageContext } from '@/web/core/evaluation/context/taskPageContext'; +import LightRowTabs from '@fastgpt/web/components/common/Tabs/LightRowTabs'; +import FolderPath from '@/components/common/folder/Path'; +import type { EvaluationStatsResponse } from '@fastgpt/global/core/evaluation/api'; +import { EvaluationStatusEnum } from '@fastgpt/global/core/evaluation/constants'; + +export enum TabEnum { + allData = 'allData', + questionData = 'questionData', + errorData = 'errorData' +} + +// 定义过滤参数类型 +export type TabFilterParams = { + status?: EvaluationStatusEnum; + belowThreshold?: boolean; +}; + +// 获取不同 tab 对应的过滤参数 +export const getTabFilterParams = (tab: TabEnum): TabFilterParams => { + switch (tab) { + case TabEnum.allData: + return {}; // 不过滤 + case TabEnum.questionData: + return { belowThreshold: true }; // 低于阈值的数据 + case TabEnum.errorData: + return { status: EvaluationStatusEnum.error }; // 错误状态的数据 + default: + return {}; + } +}; + +const NavBar = ({ + currentTab, + statsData, + onExport, + onRetryFailed +}: { + currentTab: TabEnum; + statsData: EvaluationStatsResponse | null; + onExport?: () => void; + onRetryFailed?: () => void; +}) => { + const theme = useTheme(); + const { t } = useTranslation(); + const router = useRouter(); + const query = router.query; + const { taskDetail } = useContextSelector(TaskPageContext, (v) => v); + + const tabList = useMemo(() => { + const tabConfigs = [ + { + labelKey: 'dashboard_evaluation:all_data_with_count', + value: TabEnum.allData, + count: statsData?.total || 0, + shouldShow: true + }, + { + labelKey: 'dashboard_evaluation:question_data_with_count', + value: TabEnum.questionData, + count: statsData?.failed || 0, + shouldShow: (statsData?.failed || 0) > 0 + }, + { + labelKey: 'dashboard_evaluation:error_data_with_count', + value: TabEnum.errorData, + count: statsData?.error || 0, + shouldShow: (statsData?.error || 0) > 0 + } + ]; + + return tabConfigs + .filter((config) => config.shouldShow) + .map((config) => ({ + label: t(config.labelKey, { num: config.count }), + value: config.value + })); + }, [statsData, t]); + + // 获取有效的当前选中标签,如果匹配不到则默认选中全部数据 + const validCurrentTab = useMemo(() => { + const validTabs = tabList.map((tab) => tab.value); + return validTabs.includes(currentTab) ? currentTab : TabEnum.allData; + }, [currentTab, tabList]); + + // 路径导航数据 + const paths = useMemo( + () => [{ parentId: 'current', parentName: taskDetail?.name || '-' }], + [taskDetail?.name] + ); + + const setCurrentTab = useCallback( + (tab: TabEnum) => { + router.replace({ + query: { + taskId: query.taskId, + currentTab: tab + } + }); + }, + [query, router] + ); + + const handleExport = useCallback(() => { + onExport?.(); + }, [onExport]); + + const handleRetry = useCallback(() => { + onRetryFailed?.(); + }, [onRetryFailed]); + + const handleNavigateBack = useCallback(() => { + router.push('/dashboard/evaluation?evaluationTab=tasks'); + }, [router]); + + return ( + + + + + + + + px={4} + py={1} + flex={1} + mx={'auto'} + w={'100%'} + list={tabList} + value={validCurrentTab} + activeColor="primary.700" + onChange={setCurrentTab} + inlineStyles={{ + fontSize: '1rem', + lineHeight: '1.5rem', + fontWeight: 500, + border: 'none', + _hover: { + bg: 'myGray.05' + }, + borderRadius: '6px' + }} + /> + + + + + {validCurrentTab === TabEnum.errorData && ( + + )} + + + ); +}; + +export default NavBar; diff --git a/projects/app/src/pageComponents/dashboard/evaluation/task/detail/ScoreBar.tsx b/projects/app/src/pageComponents/dashboard/evaluation/task/detail/ScoreBar.tsx new file mode 100644 index 000000000000..9c21a15c1af3 --- /dev/null +++ b/projects/app/src/pageComponents/dashboard/evaluation/task/detail/ScoreBar.tsx @@ -0,0 +1,79 @@ +import React from 'react'; +import { Box, Flex } from '@chakra-ui/react'; + +interface ScoreBarProps { + dimensionName: string; + threshold: number; + actualScore: number; + maxScore?: number; +} + +const ScoreBar: React.FC = ({ + dimensionName, + threshold, + actualScore, + maxScore = 100 +}) => { + // Check if actual score meets threshold + const isAboveThreshold = actualScore >= threshold; + + // Set color based on threshold + const scoreColor = isAboveThreshold ? 'blue.600' : 'yellow.400'; + + return ( + + {/* Dimension name */} + + {dimensionName} + + + {/* Score bar */} + + {/* Threshold line */} + + + {/* Actual score line */} + + + + {/* Score number */} + + {actualScore} + + + ); +}; + +export default ScoreBar; diff --git a/projects/app/src/pageComponents/dashboard/evaluation/task/detail/ScoreDashboard.tsx b/projects/app/src/pageComponents/dashboard/evaluation/task/detail/ScoreDashboard.tsx new file mode 100644 index 000000000000..be5d031063b7 --- /dev/null +++ b/projects/app/src/pageComponents/dashboard/evaluation/task/detail/ScoreDashboard.tsx @@ -0,0 +1,168 @@ +import React from 'react'; +import { Box, Flex } from '@chakra-ui/react'; + +interface ScoreDashboardProps { + threshold: number; + actualScore: number; + maxScore?: number; + size?: number; +} + +const ScoreDashboard: React.FC = ({ + threshold, + actualScore, + maxScore = 100, + size = 140 +}) => { + // 判断实际分数是否达到阈值 + const isAboveThreshold = actualScore >= threshold; + + const scoreColor = isAboveThreshold ? '#3370FF' : '#FDB022'; + + // 计算角度(半圆形,180度),映射到上半圆 + // 0分对应180度(9点钟),100分对应0度(3点钟) + const thresholdAngle = 180 - Math.min(Math.max((threshold / maxScore) * 180, 0), 180); + const actualScoreAngle = 180 - Math.min(Math.max((actualScore / maxScore) * 180, 0), 180); + + // 圆环参数 + const centerX = size / 2; + const centerY = size / 2; + const outerRadius = size / 2 - 8; + const outerInnerRadius = outerRadius - 24; // 外环宽度为24px + const innerOuterRadius = outerInnerRadius - 8; // 内外环间隔8px + const innerInnerRadius = innerOuterRadius - 12; // 内环宽度为12px + + // 创建上半圆弧路径(从9点钟到3点钟) + const createSemiCirclePath = (outerR: number, innerR: number) => { + return `M ${centerX - outerR} ${centerY} + A ${outerR} ${outerR} 0 0 1 ${centerX + outerR} ${centerY} + L ${centerX + innerR} ${centerY} + A ${innerR} ${innerR} 0 0 0 ${centerX - innerR} ${centerY} Z`; + }; + + // 创建弧形路径 + const createArcPath = (startAngle: number, endAngle: number, outerR: number, innerR: number) => { + // 确保角度在有效范围内 + const clampedStartAngle = Math.min(Math.max(startAngle, 0), 180); + const clampedEndAngle = Math.min(Math.max(endAngle, 0), 180); + + if (clampedStartAngle >= clampedEndAngle) return ''; + + // 转换为弧度,0度对应3点钟方向 + const startRad = clampedStartAngle * (Math.PI / 180); + const endRad = clampedEndAngle * (Math.PI / 180); + + const x1 = centerX + outerR * Math.cos(startRad); + const y1 = centerY - outerR * Math.sin(startRad); // 注意这里是减号,因为SVG的Y轴向下 + const x2 = centerX + outerR * Math.cos(endRad); + const y2 = centerY - outerR * Math.sin(endRad); + + const x3 = centerX + innerR * Math.cos(endRad); + const y3 = centerY - innerR * Math.sin(endRad); + const x4 = centerX + innerR * Math.cos(startRad); + const y4 = centerY - innerR * Math.sin(startRad); + + const largeArcFlag = clampedEndAngle - clampedStartAngle > 180 ? 1 : 0; + + return `M ${x1} ${y1} + A ${outerR} ${outerR} 0 ${largeArcFlag} 0 ${x2} ${y2} + L ${x3} ${y3} + A ${innerR} ${innerR} 0 ${largeArcFlag} 1 ${x4} ${y4} Z`; + }; + + // 创建等边三角形指针路径 + const createTrianglePointerPath = (angle: number) => { + const clampedAngle = Math.min(Math.max(angle, 0), 180); + const angleRad = clampedAngle * (Math.PI / 180); + + const sideLength = 10; + + // 三角形顶点距离黄色和蓝色填充2px + // 黄色和蓝色填充的内边缘位置是 outerRadius - 18 + const tipRadius = outerRadius - 18 - 2; + const tipX = centerX + tipRadius * Math.cos(angleRad); + const tipY = centerY - tipRadius * Math.sin(angleRad); + + // 底边中点位置(在内层圆环中间) + const baseRadius = (innerOuterRadius + innerInnerRadius) / 2; + + // 判断是否需要限制底边位置 + const score7Angle = 180 - (7 / maxScore) * 180; // 分数7对应的角度 + const score93Angle = 180 - (93 / maxScore) * 180; // 分数93对应的角度 + + let baseCenterX, baseCenterY; + + if (actualScore < 7) { + // 分数小于7时,底边固定在分数7的位置 + const fixedAngleRad = score7Angle * (Math.PI / 180); + baseCenterX = centerX + baseRadius * Math.cos(fixedAngleRad); + baseCenterY = centerY - baseRadius * Math.sin(fixedAngleRad); + } else if (actualScore > 93) { + // 分数大于93时,底边固定在分数93的位置 + const fixedAngleRad = score93Angle * (Math.PI / 180); + baseCenterX = centerX + baseRadius * Math.cos(fixedAngleRad); + baseCenterY = centerY - baseRadius * Math.sin(fixedAngleRad); + } else { + // 正常情况下,底边跟随实际分数角度 + baseCenterX = centerX + baseRadius * Math.cos(angleRad); + baseCenterY = centerY - baseRadius * Math.sin(angleRad); + } + + const halfSide = sideLength / 2; + const baseAngleRad = Math.atan2(centerY - baseCenterY, baseCenterX - centerX); + const leftX = baseCenterX + halfSide * Math.cos(baseAngleRad + Math.PI / 2); + const leftY = baseCenterY + halfSide * Math.sin(baseAngleRad + Math.PI / 2); + const rightX = baseCenterX + halfSide * Math.cos(baseAngleRad - Math.PI / 2); + const rightY = baseCenterY + halfSide * Math.sin(baseAngleRad - Math.PI / 2); + + return `M ${tipX} ${tipY} L ${leftX} ${leftY} L ${rightX} ${rightY} Z`; + }; + + return ( + + + + {/* 外层背景半圆 */} + + + {/* 外层黄色填充 */} + {thresholdAngle < 179 && ( + + )} + + {/* 外层蓝色填充 */} + {thresholdAngle > 1 && ( + + )} + + {/* 内层圆环背景 */} + + + {/* 指针 */} + + + + {/* 分数显示 */} + + {actualScore} + + + + ); +}; + +export default ScoreDashboard; diff --git a/projects/app/src/pageComponents/dataset/detail/CollectionCard/Context.tsx b/projects/app/src/pageComponents/dataset/detail/CollectionCard/Context.tsx index 1e8d95ac3f38..97644f2898a6 100644 --- a/projects/app/src/pageComponents/dataset/detail/CollectionCard/Context.tsx +++ b/projects/app/src/pageComponents/dataset/detail/CollectionCard/Context.tsx @@ -14,6 +14,11 @@ import { type DatasetCollectionsListItemType } from '@/global/core/dataset/type' import { useRouter } from 'next/router'; import { DatasetPageContext } from '@/web/core/dataset/context/datasetPageContext'; import { type WebsiteConfigFormType } from './WebsiteConfig'; +import { isEmpty } from 'lodash'; +import { useMemo } from 'react'; +import { TabEnum } from '../../../../pages/dataset/detail/index'; +import { ImportDataSourceEnum } from '@fastgpt/global/core/dataset/constants'; +import { omit } from 'lodash'; const WebSiteConfigModal = dynamic(() => import('./WebsiteConfig')); @@ -31,6 +36,12 @@ type CollectionPageContextType = { setSearchText: Dispatch>; filterTags: string[]; setFilterTags: Dispatch>; + hasDatabaseConfig: boolean; + handleOpenConfigPage: ( + mode?: 'edit' | 'create', + databaseName?: string, + activeStep?: number + ) => void; }; export const CollectionPageContext = createContext({ @@ -58,7 +69,9 @@ export const CollectionPageContext = createContext({ filterTags: [], setFilterTags: function (value: SetStateAction): void { throw new Error('Function not implemented.'); - } + }, + hasDatabaseConfig: false, + handleOpenConfigPage: () => {} }); const CollectionPageContextProvider = ({ children }: { children: ReactNode }) => { @@ -139,6 +152,29 @@ const CollectionPageContextProvider = ({ children }: { children: ReactNode }) => refreshDeps: [parentId, searchText, filterTags] }); + // database + const hasDatabaseConfig = useMemo(() => !isEmpty(datasetDetail.databaseConfig), [datasetDetail]); + const handleOpenConfigPage = ( + mode: 'edit' | 'create' = 'create', + databaseName?: string, + activeStep = 0 + ) => { + router.replace({ + query: { + ...omit(router.query, ['databaseName']), + currentTab: TabEnum.import, + source: ImportDataSourceEnum.database, + mode, + activeStep, + ...(databaseName + ? { + databaseName + } + : {}) + } + }); + }; + const contextValue: CollectionPageContextType = { openDatasetSyncConfirm: openDatasetSyncConfirm(syncDataset), onOpenWebsiteModal, @@ -153,7 +189,9 @@ const CollectionPageContextProvider = ({ children }: { children: ReactNode }) => getData, isGetting, pageNum, - pageSize + pageSize, + hasDatabaseConfig, + handleOpenConfigPage }; return ( diff --git a/projects/app/src/pageComponents/dataset/detail/CollectionCard/DatabaseListTable.tsx b/projects/app/src/pageComponents/dataset/detail/CollectionCard/DatabaseListTable.tsx index f82de3045bfd..88dd8988d48c 100644 --- a/projects/app/src/pageComponents/dataset/detail/CollectionCard/DatabaseListTable.tsx +++ b/projects/app/src/pageComponents/dataset/detail/CollectionCard/DatabaseListTable.tsx @@ -27,7 +27,7 @@ interface DatabaseListTableProps { total: number; onUpdateCollection: (params: { id: string; forbid: boolean }) => void; onTrainingStatesClick: (collectionId: string) => void; - onDataConfigClick: (collectionId: string) => void; + onDataConfigClick: (databaseName: string, activeStep: number) => void; onRemoveClick: (collectionId: string) => void; } @@ -63,7 +63,15 @@ const DatabaseListTable: React.FC = ({ {collection.name} {collection.name} + + {collection.tableSchema?.description} + + {formatTime2YMDHM(collection.createTime)} {formatTime2YMDHM(collection.updateTime)} @@ -90,6 +98,7 @@ const DatabaseListTable: React.FC = ({ onUpdateCollection({ id: collection._id, @@ -128,16 +137,36 @@ const DatabaseListTable: React.FC = ({ } menuList={[ + ...(collection.statusKey === 'ready' + ? [ + { + children: [ + { + label: t('dataset:data_config'), + icon: 'common/setting', + onClick: () => { + onDataConfigClick(collection.name, 1); + } + } + ] + } + ] + : []), { children: [ { - label: t('dataset:data_config'), - onClick: () => { - onDataConfigClick(collection._id); - } - }, - { - label: t('dataset:remove'), + label: ( + + + {t('dataset:remove')} + + ), + type: 'danger', onClick: () => { onRemoveClick(collection._id); } diff --git a/projects/app/src/pageComponents/dataset/detail/CollectionCard/EmptyCollectionTip.tsx b/projects/app/src/pageComponents/dataset/detail/CollectionCard/EmptyCollectionTip.tsx index f89a3ce92bf9..8d6c39400e26 100644 --- a/projects/app/src/pageComponents/dataset/detail/CollectionCard/EmptyCollectionTip.tsx +++ b/projects/app/src/pageComponents/dataset/detail/CollectionCard/EmptyCollectionTip.tsx @@ -11,6 +11,11 @@ const EmptyCollectionTip = () => { const { t } = useTranslation(); const onOpenWebsiteModal = useContextSelector(CollectionPageContext, (v) => v.onOpenWebsiteModal); const datasetDetail = useContextSelector(DatasetPageContext, (v) => v.datasetDetail); + const hasDatabaseConfig = useContextSelector(CollectionPageContext, (v) => v.hasDatabaseConfig); + const handleOpenConfigPage = useContextSelector( + CollectionPageContext, + (v) => v.handleOpenConfigPage + ); return ( <> @@ -54,14 +59,20 @@ const EmptyCollectionTip = () => { {datasetDetail.type === DatasetTypeEnum.database && ( - {/* TODO-lyx */} - {t('common:no_database_connection')} - {', '} - - {t('common:click_config_database')} - - + !hasDatabaseConfig ? ( + + {t('common:no_database_connection')} + handleOpenConfigPage('create')} + > + {t('common:click_config_database')} + + + ) : ( + t('common:core.dataset.collection.Empty Tip') + ) } /> )} diff --git a/projects/app/src/pageComponents/dataset/detail/CollectionCard/Header.tsx b/projects/app/src/pageComponents/dataset/detail/CollectionCard/Header.tsx index b9572d40742b..41ca16ced199 100644 --- a/projects/app/src/pageComponents/dataset/detail/CollectionCard/Header.tsx +++ b/projects/app/src/pageComponents/dataset/detail/CollectionCard/Header.tsx @@ -17,7 +17,8 @@ import { import { getDatasetCollectionPathById, postDatasetCollection, - putDatasetCollectionById + putDatasetCollectionById, + postDetectDatabaseChanges } from '@/web/core/dataset/api'; import { useTranslation } from 'next-i18next'; import MyIcon from '@fastgpt/web/components/common/Icon'; @@ -49,6 +50,7 @@ import MyBox from '@fastgpt/web/components/common/MyBox'; import Icon from '@fastgpt/web/components/common/Icon'; import MyTag from '@fastgpt/web/components/common/Tag/index'; import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip'; +import type { DetectChangesResponse } from '@fastgpt/global/core/dataset/database/api'; const FileSourceSelector = dynamic(() => import('../Import/components/FileSourceSelector')); const BackupImportModal = dynamic(() => import('./BackupImportModal')); @@ -71,7 +73,9 @@ const Header = ({ hasTrainingData }: { hasTrainingData: boolean }) => { getData, pageNum, onOpenWebsiteModal, - openDatasetSyncConfirm + openDatasetSyncConfirm, + hasDatabaseConfig, + handleOpenConfigPage } = useContextSelector(CollectionPageContext, (v) => v); const { data: paths = [] } = useRequest2(() => getDatasetCollectionPathById(parentId), { @@ -125,6 +129,123 @@ const Header = ({ hasTrainingData }: { hasTrainingData: boolean }) => { } ); + const { runAsync: onDetectDatabaseChanges, loading: isDetecting } = useRequest2( + async () => { + const result = await postDetectDatabaseChanges({ datasetId: datasetDetail._id }); + return result; + }, + { + manual: true, + errorToast: '' + } + ); + + const getTips = (data: DetectChangesResponse) => { + const { summary } = data; + if (summary?.modifiedTables > 0 && summary?.deletedTables > 0) { + return ( + <> + {t('dataset:tables_modified_and_deleted', { + modifiedTables: summary.modifiedTables, + deletedTables: summary.deletedTables + })} + {t('dataset:check_latest_data')} + + ); + } + if (summary?.modifiedTables > 0) { + return ( + <> + {t('dataset:tables_with_column_changes', { modifiedTablesCount: summary.modifiedTables })} + {t('dataset:check_latest_data')} + + ); + } + if (summary?.deletedTables > 0) { + return ( + <> + {t('dataset:tables_not_exist', { delTablesCount: summary.deletedTables })} + {t('dataset:check_latest_data')} + + ); + } + }; + + const handleRefreshDataSource = async () => { + try { + const result = await onDetectDatabaseChanges(); + + const toastId = toast({ + position: 'bottom-right', + duration: null, + render: () => ( + + + + {t('dataset:refresh_success')} + + {!(result.summary.modifiedTables > 0 || result.summary.deletedTables > 0) ? ( + t('dataset:no_data_changes') + ) : ( + + {getTips(result)} + + + )} + + + toast.close(toastId)} + /> + + ) + }); + } catch (error: any) { + const toastId = toast({ + position: 'bottom-right', + duration: null, + render: () => ( + + + + + + {t('dataset:refresh_failed')} + + {t(error?.message) || t('dataset:unknown_error')} + + + toast.close(toastId)} + /> + + ) + }); + } + }; + const isWebSite = datasetDetail?.type === DatasetTypeEnum.websiteDataset; const isDatabase = datasetDetail?.type === DatasetTypeEnum.database; @@ -142,7 +263,7 @@ const Header = ({ hasTrainingData }: { hasTrainingData: boolean }) => { return ( - + ({ @@ -192,6 +313,7 @@ const Header = ({ hasTrainingData }: { hasTrainingData: boolean }) => { {/* search input */} {isPc && ( { )} {isDatabase && ( <> - {/* TODO-lyx 是否配置字段待定 */} - {datasetDetail?.websiteConfig?.url ? ( - <> - {datasetDetail.status === DatasetStatusEnum.active && ( - - - {!hasTrainingData && feConfigs?.isPlus && ( - - )} - - )} - {datasetDetail.status === DatasetStatusEnum.syncing && ( - - {t('common:core.dataset.status.syncing')} - - )} - {datasetDetail.status === DatasetStatusEnum.waiting && ( - - {t('common:core.dataset.status.waiting')} - - )} - {datasetDetail.status === DatasetStatusEnum.error && ( - - - {t('dataset:status_error')} - - - - )} - - ) : ( + {hasDatabaseConfig ? ( <> + + ) : ( + <> )} diff --git a/projects/app/src/pageComponents/dataset/detail/CollectionCard/index.tsx b/projects/app/src/pageComponents/dataset/detail/CollectionCard/index.tsx index 43dc693672a1..ebce8ab7a370 100644 --- a/projects/app/src/pageComponents/dataset/detail/CollectionCard/index.tsx +++ b/projects/app/src/pageComponents/dataset/detail/CollectionCard/index.tsx @@ -15,7 +15,7 @@ import { HStack, Button, Alert, - AlertIcon + Text } from '@chakra-ui/react'; import { delDatasetCollectionById, @@ -72,8 +72,16 @@ const CollectionCard = () => { collectionId: string; }>(); - const { collections, Pagination, total, getData, isGetting, pageNum, pageSize } = - useContextSelector(CollectionPageContext, (v) => v); + const { + collections, + Pagination, + total, + getData, + isGetting, + pageNum, + pageSize, + handleOpenConfigPage + } = useContextSelector(CollectionPageContext, (v) => v); // Add file status icon const formatCollections = useMemo( @@ -81,10 +89,18 @@ const CollectionCard = () => { collections.map((collection) => { const icon = getCollectionIcon({ type: collection.type, name: collection.name }); const status = (() => { + if (collection.tableSchema?.hasOwnProperty('exist') && !collection.tableSchema.exist) { + return { + statusText: t('common:table_not_exist'), + colorSchema: 'gray', + statusKey: 'notExist' + }; + } if (collection.hasError) { return { statusText: t('common:core.dataset.collection.status.error'), - colorSchema: 'red' + colorSchema: 'red', + statusKey: 'error' }; } if (collection.trainingAmount > 0) { @@ -92,12 +108,14 @@ const CollectionCard = () => { statusText: t('common:dataset.collections.Collection Embedding', { total: collection.trainingAmount }), - colorSchema: 'gray' + colorSchema: 'gray', + statusKey: 'processing' }; } return { statusText: t('common:core.dataset.collection.status.active'), - colorSchema: 'green' + colorSchema: 'green', + statusKey: 'ready' }; })(); @@ -215,8 +233,10 @@ const CollectionCard = () => { {/* banner */} {isDatabase && ( - - {t('dataset:database_structure_change_tip')} + + + {t('dataset:database_structure_change_tip')} + )} {/* header */} @@ -229,10 +249,9 @@ const CollectionCard = () => { total={total} onUpdateCollection={onUpdateCollection} onTrainingStatesClick={(collectionId) => setTrainingStatesCollection({ collectionId })} - onDataConfigClick={(collectionId) => { - // TODO: 实现数据配置逻辑 - console.log('数据配置', collectionId); - }} + onDataConfigClick={(databaseName, activeStep) => + handleOpenConfigPage('edit', databaseName, activeStep) + } onRemoveClick={(collectionId) => { openDeleteConfirm( () => onDelCollection([collectionId]), diff --git a/projects/app/src/pageComponents/dataset/detail/Import/Context.tsx b/projects/app/src/pageComponents/dataset/detail/Import/Context.tsx index 333af9334654..9eec96b2215d 100644 --- a/projects/app/src/pageComponents/dataset/detail/Import/Context.tsx +++ b/projects/app/src/pageComponents/dataset/detail/Import/Context.tsx @@ -39,6 +39,7 @@ type DatasetImportContextType = { sources: ImportSourceItemType[]; setSources: React.Dispatch>; setTab: React.Dispatch>; + datasetId: string; }; export const defaultFormData: ImportFormType = { @@ -87,7 +88,8 @@ export const DatasetImportContext = createContext({ chunkOverlapRatio: 0, //@ts-ignore processParamsForm: undefined, - autoChunkSize: 0 + autoChunkSize: 0, + datasetId: '' }); const DatasetImportContextProvider = ({ children }: { children: React.ReactNode }) => { @@ -96,13 +98,17 @@ const DatasetImportContextProvider = ({ children }: { children: React.ReactNode const { source = ImportDataSourceEnum.fileLocal, parentId, - mode + mode, + activeStep: urlActiveStep } = (router.query || {}) as { source: ImportDataSourceEnum; parentId?: string; mode: 'create' | 'edit'; + activeStep?: string; }; + const datasetId = (router.query.datasetId || '') as string; + const datasetDetail = useContextSelector(DatasetPageContext, (v) => v.datasetDetail); // step @@ -224,7 +230,7 @@ const DatasetImportContextProvider = ({ children }: { children: React.ReactNode const [sources, setSources] = useState([]); - const [tab, setTab] = useState(0); + const [tab, setTab] = useState(urlActiveStep ? Number(urlActiveStep) : 0); const contextValue = { importSource: source, @@ -237,7 +243,17 @@ const DatasetImportContextProvider = ({ children }: { children: React.ReactNode processParamsForm, sources, setSources, - setTab + setTab, + datasetId + }; + + const handleReturn = () => { + router.replace({ + query: { + ...router.query, + currentTab: TabEnum.collectionCard + } + }); }; const renderCreateStatusStep = () => { @@ -253,16 +269,11 @@ const DatasetImportContextProvider = ({ children }: { children: React.ReactNode borderRadius={'50%'} variant={'whiteBase'} mr={2} - onClick={() => - router.replace({ - query: { - ...router.query, - currentTab: TabEnum.collectionCard - } - }) - } + onClick={handleReturn} /> - {t('common:Exit')} + + {t('common:Exit')} + ) : ( + + + ); + } + + const getTableBanner = () => { + if (!(isEditMode && tableChangeSummary.hasBannerTip)) return ''; + const { modifiedTables, deletedTables } = tableChangeSummary; + if (modifiedTables?.count > 0 && deletedTables?.count > 0) { + return ( + <> + + {t('dataset:tables_with_column_changes', { + modifiedTablesCount: modifiedTables.count + })} + + + {deletedTables.tableNames.map((v) => ( + + {v} + + ))} + + } + > + + {deletedTables.count} + + + {t('dataset:tables_no_longer_exist_comma')} + + ); + } + if (modifiedTables?.count > 0) { + return ( + <> + + {t('dataset:tables_with_column_changes', { + modifiedTablesCount: modifiedTables.count + })} + + + ); + } + if (deletedTables?.count > 0) { + return ( + <> + {t('dataset:found')} + + {deletedTables.tableNames.map((v) => ( + + {v} + + ))} + + } + > + + {deletedTables.count} + + + {t('dataset:tables_no_longer_exist_comma')} + + ); + } + }; + return ( {/* Edit Mode Warning Banner */} - {isEditMode && ( - - - - {t('dataset:edit_database_config_warning', { changedCount: 2, deletedCount: 2 })} - + {isEditMode && tableChangeSummary.hasBannerTip && ( + + + + {t('dataset:data_source_refreshed')} + {getTableBanner()} + {t('dataset:please_check_latest_data')} + )} - - {t('dataset:database_config_title')} - + + {!isEditMode && ( + + {t('dataset:database_config_title')} + + )} {/* Left Panel - Table Selection */} @@ -92,6 +216,7 @@ const DataBaseConfig = () => { h="100%" align="stretch" p={1} + border={theme.borders.sm} borderRadius="sm" > {/* Search Tables */} @@ -123,7 +248,7 @@ const DataBaseConfig = () => { ); return ( { colorScheme="blue" onChange={() => handleTableSelect(originalIndex)} > - {tableInfo.tableData.tableName} + + {tableInfo.tableData.tableName} + + + {problematicTableNames.includes(tableInfo.tableData.tableName) && ( + + + + )} + {tableInfo.tableData.hasColumnChanges && ( + + + + )} + ); })} @@ -148,128 +287,229 @@ const DataBaseConfig = () => { {/* Right Panel - Configuration */} - - - {/* Table Description */} - - - - * - {t('dataset:table_description')} - - - handleChangeTableDesc(e.target.value)} - placeholder={t('dataset:table_description_placeholder')} + + {currentTableColumnChanges.hasColumnChanges && ( + + + + {t('dataset:found')} + {currentTableColumnChanges.addedColumns.count > 0 && ( + <> + {currentTableColumnChanges.addedColumns.count} + {t('dataset:new_columns_added_disabled')} + + )} + {currentTableColumnChanges.deletedColumns.count > 0 && ( + + + {currentTableColumnChanges.deletedColumns.columnNames.map((v) => ( + {v} + ))} + + } + > + 0 ? '0px' : '4px'} + color={'primary.600'} + > + {currentTableColumnChanges.deletedColumns.count} + + + {t('dataset:columns_no_longer_exist')} + + )} + {t('dataset:please_check_latest_data')} + + + )} + {/* Table Description */} + + + {t('dataset:table_description')} + + span': { + display: 'block' + } + }} + > + + { + if (!currentTable?.enabled) return true; + return value?.trim() ? true : false; + } + })} + placeholder={t('dataset:table_description_placeholder')} + bg="white" + w={'100%'} + isDisabled={isOnlyRead} + /> + + + + + + {t('dataset:column_configuration')} + + + + } + onChange={(e) => setSearchColumn(e.target.value)} /> - + + - {/* Column Configuration */} - - - - - * - {t('dataset:column_configuration')} - - - - - } - onChange={(e) => setSearchColumn(e.target.value)} - /> - - - {/* Columns Table */} - - - - - - - + + ); + })} + +
{t('dataset:column_name')}{t('dataset:column_type')} - - {t('dataset:column_description')} - - {t('dataset:column_desc_accuracy_tip')} - {t('dataset:default_table_desc_tip')} - + {/* Columns Table */} + + + + + + + + + + + + + {showColumns.map((column) => { + const originalIndex = getValues('columns').findIndex( + (c) => c?.columnName === column.columnName + ); + return ( + + + + - - - - {showColumns.map((column) => { - const originalIndex = currentTable.columns.findIndex( - (c) => c.columnName === column.columnName - ); - return ( - - - - - - - ); - })} - -
+ {t('dataset:column_name')} + + {t('dataset:column_type')} + + + {t('dataset:column_description')} + + {t('dataset:column_desc_accuracy_tip')} + {t('dataset:default_table_desc_tip')} + + } + /> + + + + {t('dataset:column_enabled')} + {t('dataset:column_enabled_tip')}} /> + +
+ {column.status === StatusEnum.add ? ( + + + + {column.columnName} + + + + new + + + ) : ( + + + {column.columnName} + + + )} + + {column.columnType} + span': { + display: 'block' } - /> - - - - - {t('dataset:column_enabled')} - {t('dataset:column_enabled_tip')}} /> - -
- {column.columnName} - - {column.columnType} - - - handleChangeColumnData('description', originalIndex, e.target.value) - } + }} + > + + { + if (!currentTable?.enabled || !column.enabled) return true; + return value?.trim() ? true : false; + } + })} size="sm" - disabled={isOnlyRead} - border="1px solid" - borderColor="myGray.200" - /> - - handleColumnToggle(originalIndex)} - colorScheme="blue" - size="md" + w={'100%'} + isDisabled={isOnlyRead || !column.enabled} /> -
-
- - -
+ + +
+ handleColumnToggle(originalIndex)} + colorScheme="blue" + size="md" + /> +
+
+ + - diff --git a/projects/app/src/pageComponents/dataset/detail/Import/components/FormBottomButtons.tsx b/projects/app/src/pageComponents/dataset/detail/Import/components/FormBottomButtons.tsx index fbd51910a3f5..a197ef5ee9ca 100644 --- a/projects/app/src/pageComponents/dataset/detail/Import/components/FormBottomButtons.tsx +++ b/projects/app/src/pageComponents/dataset/detail/Import/components/FormBottomButtons.tsx @@ -1,57 +1,371 @@ -import React, { useMemo } from 'react'; -import { Box, Button, Flex, HStack } from '@chakra-ui/react'; -import { useTranslation } from 'react-i18next'; +import React, { useMemo, useState, useCallback } from 'react'; +import { Box, Button, Flex, HStack, ModalBody, VStack, Text, Spinner } from '@chakra-ui/react'; +import { useTranslation } from 'next-i18next'; import MyIcon from '@fastgpt/web/components/common/Icon'; +import MyModal from '@fastgpt/web/components/common/MyModal'; +import { useRequest2 } from '@fastgpt/web/hooks/useRequest'; +import { + updateDatasetConfig, + postCheckDatabaseConnection, + postDetectDatabaseChanges +} from '@/web/core/dataset/api'; +import type { DatabaseConfig } from '@fastgpt/global/core/dataset/type'; +import type { DatabaseFormData } from './ConnectDatabaseForm'; +import type { DetectChangesResponse } from '@fastgpt/global/core/dataset/database/api.d'; +import { useToast } from '@fastgpt/web/hooks/useToast'; + +interface ConnectionTestResult { + success: boolean; + message: string; +} + +interface DatabaseChangesInfo { + hasChanges: boolean; + addedTables: number; + modifiedTables: number; + deletedTables: number; +} interface FormBottomButtonsProps { isEditMode?: boolean; - isLoading?: boolean; - isConnecting?: boolean; - connectionError?: string; - connectionSuccess?: boolean; - onTestConnection: () => void; - onConnectAndNext: () => void; disabled?: boolean; + formData: DatabaseFormData; + datasetId: string; + onSuccess?: (isGoNext?: boolean) => void; + originalConfig?: DatabaseFormData; + beforeSubmit: (successCb: any) => any; } +const iconMap = { + success: { + icon: 'checkCircle', + color: 'green.600' + }, + fail: { + icon: 'common/error', + color: 'red.600' + } +}; + const FormBottomButtons: React.FC = ({ isEditMode = false, - isLoading = false, - isConnecting = false, - connectionError, - connectionSuccess = false, - onTestConnection, - onConnectAndNext, - disabled = false + disabled = false, + formData, + datasetId, + onSuccess, + originalConfig, + beforeSubmit }) => { const { t } = useTranslation(); + const [showConnectionModal, setShowConnectionModal] = useState(false); + const [connectionStatus, setConnectionStatus] = useState<'loading' | 'success' | 'error' | null>( + null + ); + const [connectionMessage, setConnectionMessage] = useState(''); + + // 连接测试状态 + const [connectionTest, setConnectionTest] = useState({ + success: false, + message: '' + }); + + // 数据库变更信息状态 + const [databaseChanges, setDatabaseChanges] = useState({ + hasChanges: false, + addedTables: 0, + modifiedTables: 0, + deletedTables: 0 + }); + + // 检查是否有关键配置变更(除了连接池大小) + const hasKeyConfigChanges = useMemo(() => { + if (!isEditMode || !originalConfig) return false; + + return ( + formData.host !== originalConfig.host || + formData.port !== originalConfig.port || + formData.database !== originalConfig.database || + formData.user !== originalConfig.user || + formData.password !== originalConfig.password + ); + }, [isEditMode, formData, originalConfig]); + + // 检查是否只有连接池大小变更 + const isOnlyPoolSizeChange = useMemo(() => { + if (!isEditMode || !originalConfig) return false; + + const hasPoolSizeChange = formData.poolSize !== originalConfig.poolSize; + return hasPoolSizeChange && !hasKeyConfigChanges; + }, [isEditMode, formData, originalConfig, hasKeyConfigChanges]); + + const isModifyData = useMemo( + () => !isEditMode || isOnlyPoolSizeChange || hasKeyConfigChanges, + [isOnlyPoolSizeChange, hasKeyConfigChanges, isEditMode] + ); + + const formDataString = useMemo(() => JSON.stringify(formData), [formData]); + + const { toast } = useToast(); + + React.useEffect(() => { + if (connectionTest.message || connectionTest.success) { + setConnectionTest({ + success: false, + message: '' + }); + } + connectionMessage && setConnectionMessage(''); + }, [formDataString]); + + // 连接测试请求 + const { runAsync: testConnection, loading: isConnecting } = useRequest2( + async () => { + const databaseConfig: DatabaseConfig = { + client: 'mysql', + host: formData.host, + port: formData.port, + database: formData.database, + user: formData.user, + password: formData.password, + poolSize: formData.poolSize + }; + + return await postCheckDatabaseConnection({ + datasetId, + databaseConfig + }); + }, + { + onSuccess(res: any) { + setConnectionTest({ + success: true, + message: t('dataset:connection_success') + }); + }, + onError(res: any) { + setConnectionTest({ + success: false, + message: t(res?.message) || t('dataset:connection_failed') + }); + }, + errorToast: '' + } + ); + + // 提交表单请求 + const { runAsync: onSubmitForm, loading: isSubmitting } = useRequest2( + async (data: any) => { + const databaseConfig: DatabaseConfig = { + client: 'mysql', + host: data.host, + port: data.port, + database: data.database, + user: data.user, + password: data.password, + poolSize: data.poolSize + }; + + await updateDatasetConfig({ + id: datasetId, + databaseConfig + }); + + // 如果有关键配置变更,需要检测数据库变更 + if (hasKeyConfigChanges) { + return await postDetectDatabaseChanges({ datasetId }); + } + }, + { + onSuccess(res: DetectChangesResponse | undefined) { + onSuccess?.(!isEditMode); + if (!res) { + if (isOnlyPoolSizeChange || !isModifyData) { + toast({ + title: t('common:save_success'), + status: 'success' + }); + return; + } + setConnectionStatus('success'); + setConnectionMessage(t('dataset:reconnect_success')); + return; + } + + const { hasChanges, summary } = res; + + // 存储数据库变更信息 + const changesInfo: DatabaseChangesInfo = { + hasChanges, + addedTables: summary?.addedTables || 0, + modifiedTables: summary?.modifiedTables || 0, + deletedTables: summary?.deletedTables || 0 + }; + setDatabaseChanges(changesInfo); + + setConnectionStatus('success'); + setConnectionMessage(t('dataset:reconnect_success')); + }, + onError(error) { + setConnectionStatus('error'); + setConnectionMessage(error.message || t('dataset:connection_failed')); + }, + errorToast: '' + } + ); + + const handleConnectAndNext = async (data: any) => { + if (hasKeyConfigChanges && isEditMode) { + setShowConnectionModal(true); + setConnectionStatus('loading'); + setConnectionMessage(''); + } + + await onSubmitForm(formData); + }; + + const handleCloseModal = () => { + setShowConnectionModal(false); + setConnectionStatus(null); + setConnectionMessage(''); + }; + + const renderModalContent = () => { + switch (connectionStatus) { + case 'loading': + return ( + + + + {t('dataset:reconnecting')} + + + ); + + case 'success': + const getSubTitle = () => { + if (!databaseChanges.hasChanges) { + return t('dataset:no_changes_detected'); + } + + const { modifiedTables, deletedTables } = databaseChanges; + + // 既有列变更又有表删除 + if (modifiedTables > 0 && deletedTables > 0) { + return ( + t('dataset:tables_modified_and_deleted', { + modifiedTables, + deletedTables + }) + t('dataset:please_check_latest_data') + ); + } - const editModeBtns = useMemo( - () => ( + // 只有列变更 + if (modifiedTables > 0) { + return ( + t('dataset:tables_modified', { + modifiedTables + }) + t('dataset:please_check_latest_data') + ); + } + + // 只有表删除 + if (deletedTables > 0) { + return ( + t('dataset:tables_deleted', { + deletedTables + }) + t('dataset:please_check_latest_data') + ); + } + }; + + return ( + + + + + + {t('dataset:reconnect_success')} + + + + {getSubTitle()} + {databaseChanges.hasChanges && ( + { + onSuccess?.(); + }} + > + {t('dataset:data_config')} + + )} + + + + ); + + case 'error': + return ( + + + + + + {t('dataset:connection_failed')} + + + + {t(connectionMessage)} + + + + ); + + default: + return null; + } + }; + + const editModeBtns = useMemo(() => { + const { icon, color } = iconMap[connectionTest.success ? 'success' : 'fail']; + return ( <> - {connectionError && ( + {connectionTest.message && ( <> - - {connectionError} + + {connectionTest.message} )} - + <> + + {connectionMessage && ( + <> + + {t(connectionMessage)} + + )} + + + ); - }, [onConnectAndNext, isLoading, isConnecting, disabled, t, connectionError]); + }, [handleConnectAndNext, isSubmitting, isConnecting, disabled, t, connectionTest]); + + return ( + <> + {isEditMode ? editModeBtns : createModeBtns} - return isEditMode ? editModeBtns : createModeBtns; + {/* Connection Status Modal - Only show in edit mode with key config changes */} + {isEditMode && hasKeyConfigChanges && ( + + {renderModalContent()} + + )} + + ); }; export default FormBottomButtons; diff --git a/projects/app/src/pageComponents/dataset/detail/Import/components/hooks/useDataBaseConfig.ts b/projects/app/src/pageComponents/dataset/detail/Import/components/hooks/useDataBaseConfig.ts index a084002c9ecf..4a33e01b3f25 100644 --- a/projects/app/src/pageComponents/dataset/detail/Import/components/hooks/useDataBaseConfig.ts +++ b/projects/app/src/pageComponents/dataset/detail/Import/components/hooks/useDataBaseConfig.ts @@ -1,271 +1,451 @@ -import { useState } from 'react'; +import { useState, useEffect, useMemo, useCallback, useRef } from 'react'; import { useTranslation } from 'next-i18next'; -import { mockData } from '../const'; - -// 后端数据结构定义 -interface BackendColumn { - columnName: string; - columnType: string; - description: string; - examples: string[]; - enabled: boolean; - valueIndex: boolean; +import { useRouter } from 'next/router'; +import type { + UseFormGetValues, + UseFormSetValue, + UseFormWatch, + UseFormReset +} from 'react-hook-form'; +import { useRequest2 } from '@fastgpt/web/hooks/useRequest'; +import { + postGetDatabaseConfiguration, + postDetectDatabaseChanges, + postCreateDatabaseCollections, + postUpdateDatasetCollectionConfigByDatabase +} from '@/web/core/dataset/api'; +import type { DetectChangesResponse } from '@/web/core/dataset/temp.d'; +import type { + UIColumn, + UITableData, + TableInfo, + CurrentTableFormData, + TableChangeSummary, + CurrentTableColumnChanges +} from './utils'; +import { + transformBackendToUI, + transformChangesToUI, + transformUIToBackend, + getProblematicTableNames, + StatusEnum +} from './utils'; + +interface FormMethods { + getValues: UseFormGetValues; + setValue: UseFormSetValue; + watch: UseFormWatch; + reset: UseFormReset; } -interface BackendTableData { - tableName: string; - description: string; - enabled: boolean; - columns: Record; -} - -// 前端UI使用的数据结构 -interface UIColumn { - columnName: string; - columnType: string; - description: string; - examples: string[]; - enabled: boolean; - valueIndex: boolean; -} +export const useDataBaseConfig = ( + datasetId: string, + isEditMode: boolean = false, + formMethods: FormMethods +) => { + const { t } = useTranslation(); + const router = useRouter(); + const { getValues, setValue, watch, reset } = formMethods; -interface UITableData { - tableName: string; - description: string; - enabled: boolean; - columns: UIColumn[]; -} + const [uiTables, setUITables] = useState([]); + const [currentTableIndex, setCurrentTableIndex] = useState(0); + const [changesSummary, setChangesSummary] = useState( + null + ); + const [tableChangeSummary, setTableChangeSummary] = useState({ + modifiedTables: { count: 0, tableNames: [] }, + deletedTables: { count: 0, tableNames: [] }, + addedTables: { count: 0, tableNames: [] }, + hasChanges: false, + hasBannerTip: false + }); -interface TableInfo { - tableData: UITableData; - isCurrentTable: boolean; -} + // 监听表单数据变化 + const watchedCurrentTable = watch(); -// 数据预处理函数:将后端数据转换为UI数据 -const transformBackendToUI = (backendTables: BackendTableData[]): UITableData[] => { - return backendTables.map((table) => ({ - tableName: table.tableName, - description: table.description, - enabled: table.enabled, - columns: Object.values(table.columns).map((column) => ({ - columnName: column.columnName, - columnType: column.columnType, - description: column.description, - enabled: column.enabled, - examples: column.examples, - valueIndex: column.valueIndex - })) - })); -}; + // 获取当前表 + const currentTable = useMemo(() => { + const data = uiTables[currentTableIndex]; + if (data?.hasOwnProperty('exist')) { + return uiTables[currentTableIndex]?.exist ? uiTables[currentTableIndex] : null; + } + return uiTables[currentTableIndex] || null; + }, [uiTables, currentTableIndex]); -// 数据转换函数:将UI数据转换为后端数据 -const transformUIToBackend = (uiTables: UITableData[]): BackendTableData[] => { - return uiTables.map((table) => ({ - tableName: table.tableName, - description: table.description, - enabled: table.enabled, - columns: table.columns.reduce( - (acc, column) => { - acc[column.columnName] = { - columnName: column.columnName, - columnType: column.columnType, - description: column.description, - enabled: column.enabled, - examples: column.examples, - valueIndex: column.valueIndex - }; - return acc; - }, - {} as Record - ) - })); -}; + // 获取数据配置 + const { runAsync: getConfiguration, loading: getConfigLoading } = useRequest2( + postGetDatabaseConfiguration + ); -export const useDataBaseConfig = () => { - const { t } = useTranslation(); + // 检测变更 + const { runAsync: detectChanges, loading: detectChangesLoading } = + useRequest2(postDetectDatabaseChanges); - // 模拟后端数据 - const mockBackendData: BackendTableData[] = mockData as BackendTableData[]; + // 创建数据库知识库数据集 + const { runAsync: createCollections, loading: isCreating } = useRequest2( + postCreateDatabaseCollections, + { + onSuccess: () => { + router.push(`/dataset/detail?datasetId=${datasetId}&forceUpdate=true`); + } + } + ); - // 初始化UI数据 - const [uiTables, setUITables] = useState(() => - transformBackendToUI(mockBackendData) + // 更新数据库配置 + const { runAsync: updateDatabaseConfig, loading: isUpdating } = useRequest2( + postUpdateDatasetCollectionConfigByDatabase, + { + onSuccess: () => { + router.push(`/dataset/detail?datasetId=${datasetId}`); + }, + successToast: t('common:update_success') + } ); - const [tableInfos, setTableInfos] = useState(() => - uiTables.map((table, index) => ({ + // 动态计算表信息 + const tableInfos = useMemo(() => { + return uiTables.map((table, index) => ({ tableData: table, - isCurrentTable: index === 0 - })) - ); + isCurrentTable: index === currentTableIndex + })); + }, [uiTables, currentTableIndex]); + + // 动态计算loading状态 + const loading = useMemo(() => { + return getConfigLoading || detectChangesLoading; + }, [getConfigLoading, detectChangesLoading]); + + // 动态计算提交按钮loading状态 + const isSubmitting = useMemo(() => { + return isCreating || isUpdating; + }, [isCreating, isUpdating]); + + // 动态计算存在问题的表名(表已勾选且表描述为空或列启用但描述为空) + const problematicTableNames = useMemo(() => { + return getProblematicTableNames(uiTables); + }, [JSON.stringify(uiTables), uiTables]); + + // 计算当前表格的列变更信息 + const currentTableColumnChanges = useMemo(() => { + if (!currentTable) { + return { + addedColumns: { count: 0, columnNames: [] }, + deletedColumns: { count: 0, columnNames: [] }, + hasColumnChanges: false + }; + } - const [currentTable, setCurrentTable] = useState(uiTables[0]); + const addedColumnNames: string[] = []; + const deletedColumnNames: string[] = []; - // 验证错误状态 - const [validationErrors, setValidationErrors] = useState<{ - tableDescription: string; - columnDescriptions: Record; - }>({ - tableDescription: '', - columnDescriptions: {} - }); + currentTable.columns.forEach((column) => { + if (column.status === StatusEnum.add) { + addedColumnNames.push(column.columnName); + } else if (column.status === StatusEnum.delete) { + deletedColumnNames.push(column.columnName); + } + }); + + return { + addedColumns: { + count: addedColumnNames.length, + columnNames: addedColumnNames + }, + deletedColumns: { + count: deletedColumnNames.length, + columnNames: deletedColumnNames + }, + hasColumnChanges: addedColumnNames.length > 0 || deletedColumnNames.length > 0 + }; + }, [currentTable]); + + // 同步当前表单数据到uiTables + const syncCurrentTableToUITables = useCallback(() => { + if (!currentTable) return; - // 验证表描述 - const validateTableDescription = (table: UITableData) => { - if (table.enabled && !table.description.trim()) { - return t('dataset:table_description_required'); + const formData = getValues(); + const updatedTables = [...uiTables]; + const newTableData = { + ...currentTable, + description: formData.description, + columns: formData.columns + }; + + // 只有数据真正发生变化时才更新状态 + if (JSON.stringify(updatedTables[currentTableIndex]) !== JSON.stringify(newTableData)) { + updatedTables[currentTableIndex] = newTableData as any; + setUITables(updatedTables); } - return ''; - }; + }, [currentTable, currentTableIndex, uiTables, getValues]); + + // 初始化数据 + useEffect(() => { + const initData = async () => { + try { + let uiData: UITableData[] = []; + + if (isEditMode) { + // 编辑模式:只检测变更 + const changesResult = await detectChanges({ datasetId }); + const changesUIData = transformChangesToUI(changesResult.tables); + + uiData = changesUIData.map((table) => ({ + ...table, + // 新增表默认不勾选 + enabled: table.status === StatusEnum.add ? false : true, + columns: table.columns.map((col) => ({ + ...col, + // 新增列默认不启用 + enabled: col.status === StatusEnum.add ? false : col.enabled + })) + })); + + setChangesSummary(changesResult.summary); + if (changesResult.hasChanges) { + // 只有编辑模式才计算表格变更汇总信息并设置hasColumnChanges字段 + const modifiedTableNames: string[] = []; + const deletedTableNames: string[] = []; + const addedTableNames: string[] = []; + + uiData = uiData.map((table) => { + // 检查是否有列变更 + const hasColumnChanges = table.columns.some( + (col) => col.status === StatusEnum.add || col.status === StatusEnum.delete + ); + + if (table.status === StatusEnum.delete) { + deletedTableNames.push(table.tableName); + } else if (table.status === StatusEnum.add) { + addedTableNames.push(table.tableName); + } else if (hasColumnChanges) { + modifiedTableNames.push(table.tableName); + } + + return { + ...table, + hasColumnChanges + }; + }); + + setTableChangeSummary({ + modifiedTables: { + count: modifiedTableNames.length, + tableNames: modifiedTableNames + }, + deletedTables: { + count: deletedTableNames.length, + tableNames: deletedTableNames + }, + addedTables: { + count: addedTableNames.length, + tableNames: addedTableNames + }, + hasChanges: + modifiedTableNames.length > 0 || + deletedTableNames.length > 0 || + addedTableNames.length > 0, + hasBannerTip: modifiedTableNames.length > 0 || deletedTableNames.length > 0 + }); + } + } else { + // 创建模式:获取数据配置 + const configResult = await getConfiguration({ datasetId }); + uiData = transformBackendToUI(configResult.tables); + // 创建模式:设置默认的表格变更汇总信息 + setTableChangeSummary({ + modifiedTables: { count: 0, tableNames: [] }, + deletedTables: { count: 0, tableNames: [] }, + addedTables: { count: 0, tableNames: [] }, + hasChanges: false, + hasBannerTip: false + }); + } + setUITables(uiData); + + // 检查地址栏中是否存在 databaseName 参数 + const urlParams = new URLSearchParams(window.location.search); + const databaseName = urlParams.get('databaseName'); - // 验证列描述 - const validateColumnDescription = (column: UIColumn) => { - if (column.enabled && !column.description.trim()) { - return t('dataset:column_description_required'); + let targetTableIndex = -1; + + if (databaseName) { + // 查找匹配的表索引 + targetTableIndex = uiData.findIndex( + (table) => table.tableName === databaseName && table.status !== StatusEnum.delete + ); + } + + // 如果没有找到指定的表或没有 databaseName 参数,则找到第一个未删除的表作为默认选中 + if (targetTableIndex === -1) { + targetTableIndex = uiData.findIndex((table) => table.status !== StatusEnum.delete); + } + + setCurrentTableIndex(targetTableIndex >= 0 ? targetTableIndex : 0); + + // 初始化表单数据 + const targetTable = uiData[targetTableIndex >= 0 ? targetTableIndex : 0]; + if (targetTable) { + reset({ + description: targetTable.description, + columns: targetTable.columns.filter((col) => col.status !== StatusEnum.delete) + }); + + // 标记初始化完成 + isInitializedRef.current = true; + lastSyncDataRef.current = JSON.stringify({ + description: targetTable.description, + columns: targetTable.columns.filter((col) => col.status !== StatusEnum.delete) + }); + } + } catch (error) { + console.error('init_data_failed', error); + } + }; + + if (datasetId) { + initData(); } - return ''; - }; + }, [datasetId, isEditMode, reset, t]); + + // 使用 ref 来避免不必要的重新渲染 + const isInitializedRef = useRef(false); + const lastSyncDataRef = useRef(''); + + // 监听表单数据变化,同步到uiTables + useEffect(() => { + if (!currentTable || !isInitializedRef.current) return; + + const currentSyncData = JSON.stringify(watchedCurrentTable); + if (lastSyncDataRef.current !== currentSyncData) { + lastSyncDataRef.current = currentSyncData; + syncCurrentTableToUITables(); + } + }, [watchedCurrentTable, currentTable, syncCurrentTableToUITables]); // 切换表的启用状态 const handleTableSelect = (index: number) => { + // 先同步当前表单数据 + syncCurrentTableToUITables(); + const updatedTables = [...uiTables]; updatedTables[index].enabled = !updatedTables[index].enabled; setUITables(updatedTables); - const updatedTableInfos = [...tableInfos]; - updatedTableInfos[index].tableData = updatedTables[index]; - setTableInfos(updatedTableInfos); - - // 如果当前编辑的是这个表,更新当前表数据并验证 - if (tableInfos[index].isCurrentTable) { - setCurrentTable(updatedTables[index]); - - // 验证表描述 - const tableDescError = validateTableDescription(updatedTables[index]); - setValidationErrors((prev) => ({ - ...prev, - tableDescription: tableDescError - })); + // 如果是当前表,同步到表单 + if (index === currentTableIndex) { + setValue('description', updatedTables[index].description); } }; // 切换当前编辑的表 - const handleChangeTab = (index: number) => { - const currentTableIndex = tableInfos.findIndex((info) => info.isCurrentTable); - if (index === currentTableIndex) return; + const handleChangeTab = useCallback( + (index: number) => { + if (currentTableIndex === index) return; - // 保存当前表的修改 - const updatedTables = [...uiTables]; - updatedTables[currentTableIndex] = currentTable; - setUITables(updatedTables); + // 先同步当前表单数据 + syncCurrentTableToUITables(); - // 更新表信息 - const updatedTableInfos = tableInfos.map((info, i) => ({ - ...info, - isCurrentTable: i === index, - tableData: updatedTables[i] - })); - setTableInfos(updatedTableInfos); + // 切换到新表 + setCurrentTableIndex(index); - // 设置新的当前表 - setCurrentTable(updatedTables[index]); - }; + // 同步新表数据到表单,已删除的列数据提交时过滤 + if (uiTables[index]) { + const newFormData = { + description: uiTables[index].description, + columns: uiTables[index].columns + }; + reset(newFormData); + lastSyncDataRef.current = JSON.stringify(newFormData); + } + }, + [currentTableIndex, syncCurrentTableToUITables, uiTables, reset] + ); // 修改表描述 const handleChangeTableDesc = (value: string) => { - const updatedTable = { - ...currentTable, - description: value - }; - setCurrentTable(updatedTable); - - // 验证表描述 - const error = validateTableDescription(updatedTable); - setValidationErrors((prev) => ({ - ...prev, - tableDescription: error - })); + setValue('description', value); }; // 修改列信息 const handleChangeColumnData = ( key: K, - index: number, + columnIndex: number, value: UIColumn[K] ) => { - const updatedColumns = [...currentTable.columns]; - updatedColumns[index][key] = value; - const updatedTable = { - ...currentTable, - columns: updatedColumns + const currentColumns = getValues('columns'); + const updatedColumns = [...currentColumns]; + updatedColumns[columnIndex] = { + ...updatedColumns[columnIndex], + [key]: value }; - setCurrentTable(updatedTable); - - // 如果修改的是描述字段,需要验证 - if (key === 'description') { - const error = validateColumnDescription(updatedColumns[index]); - console.log(error); - setValidationErrors((prev) => ({ - ...prev, - columnDescriptions: { - ...prev.columnDescriptions, - [index]: error - } - })); - } + setValue('columns', updatedColumns); }; // 切换列的启用状态 - const handleColumnToggle = (index: number) => { - const newEnabledState = !currentTable.columns[index].enabled; - handleChangeColumnData('enabled', index, newEnabledState); - - // 验证列描述 - const updatedColumn = { ...currentTable.columns[index], enabled: newEnabledState }; - const error = validateColumnDescription(updatedColumn); - setValidationErrors((prev) => ({ - ...prev, - columnDescriptions: { - ...prev.columnDescriptions, - [index]: error - } - })); + const handleColumnToggle = (columnIndex: number) => { + const currentColumn = getValues(`columns.${columnIndex}`); + setValue(`columns.${columnIndex}.enabled`, !currentColumn.enabled); }; // 切换列的索引状态 - const handleValueIndexToggle = (index: number) => { - handleChangeColumnData('valueIndex', index, !currentTable.columns[index].valueIndex); + const handleValueIndexToggle = (columnIndex: number) => { + const currentColumn = getValues(`columns.${columnIndex}`); + setValue(`columns.${columnIndex}.valueIndex`, !currentColumn.valueIndex); }; - const handleConfirm = () => { - // 保存当前表的修改 - const finalTables = [...uiTables]; - const currentTableIndex = tableInfos.findIndex((info) => info.isCurrentTable); - finalTables[currentTableIndex] = currentTable; + // 表单提交处理 + const onSubmit = async () => { + // 先同步当前表单数据 + syncCurrentTableToUITables(); + // 校验是否存在问题表 + if (problematicTableNames.length > 0) { + // 找到第一个有问题的表的索引 + const firstProblematicTableIndex = uiTables.findIndex((table) => + problematicTableNames.includes(table.tableName) + ); + + if (firstProblematicTableIndex !== -1) { + // 自动切换到第一个不满足的表 + handleChangeTab(firstProblematicTableIndex); + } - // 转换为后端格式 - const backendData = transformUIToBackend(finalTables); - console.log('Database config confirmed:', backendData); + return; // 阻止提交 + } - // 这里可以调用API提交数据 - // await submitDatabaseConfig(backendData); + // 转换为后端格式 + const backendData = transformUIToBackend(uiTables); + + if (isEditMode) { + // 编辑模式:调用更新数据库配置接口 + await updateDatabaseConfig({ datasetId, ...backendData }); + } else { + // 创建模式:调用创建数据库知识库数据集接口 + await createCollections({ datasetId, ...backendData }); + } }; return { // 状态 currentTable, + currentTableIndex, + uiTables, tableInfos, - validationErrors, - + loading, + isCreating, + isUpdating, + isSubmitting, + changesSummary, + problematicTableNames, + tableChangeSummary, + currentTableColumnChanges, + + // 方法 handleTableSelect, handleChangeTab, handleChangeTableDesc, handleChangeColumnData, handleColumnToggle, handleValueIndexToggle, - handleConfirm + onSubmit }; }; - -// 导出类型定义 -export type { UIColumn, UITableData, BackendColumn, BackendTableData, TableInfo }; diff --git a/projects/app/src/pageComponents/dataset/detail/Import/components/hooks/utils.ts b/projects/app/src/pageComponents/dataset/detail/Import/components/hooks/utils.ts new file mode 100644 index 000000000000..506e7e330719 --- /dev/null +++ b/projects/app/src/pageComponents/dataset/detail/Import/components/hooks/utils.ts @@ -0,0 +1,223 @@ +import type { + CreateDatabaseCollectionsBody, + DBTableChange, + TableStatusEnum +} from '@/web/core/dataset/temp.d'; +import { i18nT } from '@fastgpt/web/i18n/utils'; +import type { + DatabaseCollectionsTable, + DatabaseCollectionsBody, + DetectChangesResponse +} from '@fastgpt/global/core/dataset/database/api'; + +import type { ColumnSchemaType } from '@fastgpt/global/core/dataset/type'; + +export enum StatusEnum { + add = 'add', + delete = 'delete', + available = 'available' +} + +// 后端数据结构定义 +export interface BackendColumn { + columnName: string; + columnType: string; + description: string; + examples: string[]; + forbid: boolean; + valueIndex: boolean; +} + +export interface BackendTableData { + tableName: string; + description: string; + forbid: boolean; + columns: Record; +} + +// 前端UI使用的数据结构 +export interface UIColumn { + columnName: string; + columnType: string; + description: string; + examples: string[]; + enabled: boolean; + valueIndex: boolean; + status: StatusEnum; +} + +export type UITableData = Omit & { + tableName: string; + description: string; + enabled: boolean; + columns: (ColumnSchemaType & { enabled: boolean; status: StatusEnum })[]; + status: StatusEnum; + hasColumnChanges: boolean; +}; + +export interface TableInfo { + tableData: UITableData; + isCurrentTable: boolean; +} + +// 表单数据结构 - 只包含当前表的数据 +export interface CurrentTableFormData { + description: string; + columns: UIColumn[]; +} + +// 表格变更汇总信息 +export interface TableChangeSummary { + // 存在列变更的表 + modifiedTables: { + count: number; + tableNames: string[]; + }; + // 已删除的表 + deletedTables: { + count: number; + tableNames: string[]; + }; + // 新增的表 + addedTables: { + count: number; + tableNames: string[]; + }; + // 是否有任何变更 + hasChanges: boolean; + // 是否需要横幅提示(新增不需要提示) + hasBannerTip: boolean; +} + +// 当前表格的列变更信息 +export interface CurrentTableColumnChanges { + // 新增列 + addedColumns: { + count: number; + columnNames: string[]; + }; + // 删除列 + deletedColumns: { + count: number; + columnNames: string[]; + }; + // 是否有列变更 + hasColumnChanges: boolean; +} + +// 原有的完整表单数据结构(保留用于兼容) +export interface DatabaseFormData { + tables: UITableData[]; + currentTableIndex: number; +} + +// 数据预处理函数:将后端数据转换为UI数据 +export const transformBackendToUI = ( + backendTables: DatabaseCollectionsBody['tables'] +): UITableData[] => { + return backendTables.map((table) => ({ + ...table, + tableName: table.tableName, + description: table.description, + enabled: !table.forbid, + status: StatusEnum.available, + hasColumnChanges: false, + columns: Object.values(table.columns).map((column) => ({ + ...column, + columnName: column.columnName, + columnType: column.columnType, + description: column.description, + enabled: !column.forbid, + examples: column.examples, + valueIndex: column.valueIndex, + status: StatusEnum.available + })) + })); +}; + +// 数据转换函数:将变更检测数据转换为UI数据 +export const transformChangesToUI = ( + backendTables: DetectChangesResponse['tables'] +): UITableData[] => { + return backendTables.map((table) => { + const columns = Object.values(table.columns).map((column: any) => ({ + ...column, + columnName: column.columnName, + columnType: column.columnType, + description: column.description, + examples: column.examples, + enabled: !column.forbid, + valueIndex: column.valueIndex, + status: column.status + })); + + // 检查是否有列变更 + const hasColumnChanges = columns.some( + (col) => col.status === StatusEnum.add || col.status === StatusEnum.delete + ); + + return { + ...table, + tableName: table.tableName, + description: table.description, + enabled: table.enabled, + columns, + status: table.status, + hasColumnChanges + }; + }) as unknown as UITableData[]; +}; + +// 数据转换函数:将UI数据转换为后端数据 +export const transformUIToBackend = (uiTables: UITableData[]): DatabaseCollectionsBody => { + return { + tables: uiTables + .filter((table) => table.enabled && table.status !== StatusEnum.delete) + .map((table) => ({ + ...table, + tableName: table.tableName, + description: table.description, + forbid: table.forbid, + columns: table.columns + .filter((column) => column.status !== StatusEnum.delete) + .reduce( + (acc, column) => { + acc[column.columnName] = { + ...column, + columnName: column.columnName, + columnType: column.columnType, + description: column.description, + examples: column.examples, + forbid: !column.enabled, + valueIndex: column.valueIndex + }; + return acc; + }, + {} as Record + ) + })) + }; +}; + +// 数据转换函数:将表单数据转换为后端数据(保留用于兼容) +export const transformFormDataToBackend = (formData: DatabaseFormData): DatabaseCollectionsBody => { + return transformUIToBackend(formData.tables); +}; + +// 计算存在问题的表名(表已勾选且表描述为空或列启用但描述为空) +export const getProblematicTableNames = (uiTables: UITableData[]): string[] => { + return uiTables + .filter((table) => { + // 条件1:表已勾选 + if (!table.enabled) return false; + + // 条件2:表描述为空 或者 数据列配置中启用状态为true但是描述为空 + const hasEmptyTableDesc = !table.description.trim(); + const hasEnabledColumnsWithEmptyDesc = table.columns.some( + (column) => column.enabled && !column.description.trim() + ); + + return hasEmptyTableDesc || hasEnabledColumnsWithEmptyDesc; + }) + .map((table) => table.tableName); +}; diff --git a/projects/app/src/pageComponents/dataset/detail/Import/utils.tsx b/projects/app/src/pageComponents/dataset/detail/Import/utils.tsx index b8aafb7623ac..d276a37da63c 100644 --- a/projects/app/src/pageComponents/dataset/detail/Import/utils.tsx +++ b/projects/app/src/pageComponents/dataset/detail/Import/utils.tsx @@ -24,14 +24,65 @@ export default function Dom() { } export const databaseAddrValidator = (val: string) => { + // 如果为空,返回错误 + if (!val || val.trim() === '') { + return i18nT('dataset:database_address_required'); + } + + const trimmedVal = val.trim(); + + // 禁止的地址模式 - 更精确的匹配 const forbiddenPatterns = [ - /^127(\.\d+){0,3}$/, // 匹配127、127.x、127.x.x、127.x.x.x + /^127\.(\d{1,3}\.){2}\d{1,3}$/, // 127.x.x.x 格式的IP /^localhost$/i, // localhost - /^0\.0\.0\.0$/ // 0.0.0.0 + /^0\.0\.0\.0$/, // 0.0.0.0 + /^::1$/, // IPv6 localhost + /^::$/ // IPv6 任意地址 ]; - if (forbiddenPatterns.some((pattern) => pattern.test(val))) { + + if (forbiddenPatterns.some((pattern) => pattern.test(trimmedVal))) { return i18nT('dataset:validate_ip_tip'); } - return true; + // IPv4 地址格式校验(宽松) + const ipv4Pattern = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/; + const ipv4Match = trimmedVal.match(ipv4Pattern); + + if (ipv4Pattern.test(trimmedVal)) { + if (ipv4Match) { + // 检查每个段是否在0-255范围内 + const segments = ipv4Match.slice(1, 5).map(Number); + if (segments.some((segment) => segment > 255)) { + return i18nT('dataset:ip_format_invalid_range'); + } + + // 再次检查是否是127开头的IP + if (segments[0] === 127) { + return i18nT('dataset:validate_ip_tip'); + } + + return true; + } + } + + // IPv6 地址格式校验(宽松) + const ipv6Pattern = + /^([0-9a-fA-F]{0,4}:){1,7}[0-9a-fA-F]{0,4}$|^([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}$/; + if (ipv6Pattern.test(trimmedVal)) { + return true; + } + + // 域名格式校验(宽松)- 排除看起来像IP但格式不对的字符串 + if (/^\d+\.\d+\.\d+\.\d+/.test(trimmedVal)) { + return i18nT('dataset:ip_format_invalid_range'); + } + + const domainPattern = + /^[a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?)*$/; + if (domainPattern.test(trimmedVal)) { + return true; + } + + // 如果都不匹配,返回格式错误 + return i18nT('dataset:database_address_format_invalid'); }; diff --git a/projects/app/src/pageComponents/dataset/detail/InputDataModal.tsx b/projects/app/src/pageComponents/dataset/detail/InputDataModal.tsx index daf54fa66904..bcf6237846ff 100644 --- a/projects/app/src/pageComponents/dataset/detail/InputDataModal.tsx +++ b/projects/app/src/pageComponents/dataset/detail/InputDataModal.tsx @@ -29,6 +29,7 @@ import FillRowTabs from '@fastgpt/web/components/common/Tabs/FillRowTabs'; import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel'; import MyIconButton from '@fastgpt/web/components/common/Icon/button'; import MyImage from '@/components/MyImage/index'; +import { postCreateEvaluationDatasetData } from '@/web/core/evaluation/dataset'; export type InputDataType = { q: string; @@ -51,13 +52,15 @@ const InputDataModal = ({ dataId, defaultValue, onClose, - onSuccess + onSuccess, + evaluationDatasetId }: { collectionId: string; dataId?: string; defaultValue?: { q?: string; a?: string; imagePreivewUrl?: string }; + evaluationDatasetId?: string; onClose: () => void; - onSuccess: (data: InputDataType & { dataId: string }) => void; + onSuccess: (data: InputDataType & { dataId?: string }) => void; }) => { const { t } = useTranslation(); const { embeddingModelList, defaultModels } = useSystemStore(); @@ -79,7 +82,7 @@ const InputDataModal = ({ const { data: collection = defaultCollectionDetail, loading: initLoading } = useRequest2( async () => { const [collection, dataItem] = await Promise.all([ - getDatasetCollectionById(collectionId), + collectionId ? getDatasetCollectionById(collectionId) : Promise.resolve(), ...(dataId ? [getDatasetDataItemById(dataId)] : []) ]); @@ -106,7 +109,7 @@ const InputDataModal = ({ } // Forcus reset to image tab - if (collection.type === DatasetCollectionTypeEnum.images) { + if (collection?.type === DatasetCollectionTypeEnum.images) { setCurrentTab(TabEnum.image); } return collection; @@ -121,20 +124,34 @@ const InputDataModal = ({ const { runAsync: sureImportData, loading: isImporting } = useRequest2( async (e: InputDataType) => { const data = { ...e }; + let dataId: string | undefined; - const postData: any = { - collectionId: collection._id, - q: e.q, - a: currentTab === TabEnum.qa ? e.a : '', - // Contains no default index - indexes: e.indexes?.filter((item) => !!item.text?.trim()) || [] - }; + // 如果存在evaluationDatasetId,调用评估数据集接口 + if (evaluationDatasetId) { + await postCreateEvaluationDatasetData({ + collectionId: evaluationDatasetId, + userInput: e.q, + expectedOutput: e.a, + enableQualityEvaluation: false + }); + } + + // 如果存在collectionId,调用普通数据集接口 + if (collectionId) { + const postData: any = { + collectionId: collection._id, + q: e.q, + a: currentTab === TabEnum.qa ? e.a : '', + // Contains no default index + indexes: e.indexes?.filter((item) => !!item.text?.trim()) || [] + }; - const dataId = await postInsertData2Dataset(postData); + dataId = await postInsertData2Dataset(postData); + } return { ...data, - dataId + ...(dataId && { dataId }) }; }, { @@ -196,18 +213,15 @@ const InputDataModal = ({ return vectorModel?.maxToken || 2000; }, [collection.dataset.vectorModel, defaultModels.embedding, embeddingModelList]); - return ( - onClose()} - closeOnOverlayClick={false} - maxW={'1440px'} - h={'46.25rem'} - title={ + const modalTitle = useMemo( + () => ( + <> - + - {collection.sourceName || t('common:unknow_source')} + {evaluationDatasetId + ? t('common:annotation_answer') + : collection.sourceName || t('common:unknow_source')} - } + + ), + [collection.sourceName, icon, evaluationDatasetId, t] + ); + + return ( + onClose()} + closeOnOverlayClick={false} + maxW={'1440px'} + h={'46.25rem'} + title={modalTitle} > {/* Tab */} - {(currentTab === TabEnum.chunk || currentTab === TabEnum.qa) && ( + {(currentTab === TabEnum.chunk || currentTab === TabEnum.qa) && !evaluationDatasetId && ( { /> setSelectFiles(e)} /> {/* 渲染已选择的文件 */} @@ -388,15 +258,10 @@ const FileImport = () => { - {item.sourceName} + {item.name} - {item.sourceSize} + {item.size} - {item.isUploading && ( - - - - )} { }} /> - {/* 显示错误信息 */} - {item.errorMsg && ( - - {t('common:error')}: {item.errorMsg} - + /> )} ))} @@ -473,10 +334,14 @@ const FileImport = () => { h={9} type="submit" form="file-import-form" - isDisabled={!isFormValid || uploading || errorFiles.length > 0} + isDisabled={!isFormValid} isLoading={isSubmitting} > - {t('dashboard_evaluation:file_import_confirm')} + {isSubmitting + ? percent === 100 + ? t('dataset:data_parsing') + : t('dataset:data_uploading', { num: percent }) + : t('dashboard_evaluation:file_import_confirm')} @@ -491,7 +356,7 @@ export default FileImport; export async function getServerSideProps(content: any) { return { props: { - ...(await serviceSideProps(content, ['dashboard_evaluation', 'file'])) + ...(await serviceSideProps(content, ['dashboard_evaluation', 'evaluation', 'file'])) } }; } diff --git a/projects/app/src/pages/dashboard/evaluation/dataset/index.tsx b/projects/app/src/pages/dashboard/evaluation/dataset/index.tsx index eeeb2c825807..2db1e286607a 100644 --- a/projects/app/src/pages/dashboard/evaluation/dataset/index.tsx +++ b/projects/app/src/pages/dashboard/evaluation/dataset/index.tsx @@ -92,7 +92,7 @@ const EvaluationDatasets = ({ Tab }: { Tab: React.ReactNode }) => { }, processing: { label: t('dashboard_evaluation:data_generating'), - colorSchema: 'primary.600' + colorSchema: 'blue' } }; @@ -219,7 +219,7 @@ const EvaluationDatasets = ({ Tab }: { Tab: React.ReactNode }) => { { label: ( - + {t('dashboard_evaluation:smart_generation')} ), @@ -348,8 +348,8 @@ const EvaluationDatasets = ({ Tab }: { Tab: React.ReactNode }) => { /> )} - - + + {/* 智能生成数据集弹窗 */} {isIntelligentModalOpen && ( diff --git a/projects/app/src/pages/dashboard/evaluation/dimension/index.tsx b/projects/app/src/pages/dashboard/evaluation/dimension/index.tsx index ade7c9dc3798..d52c5e0a8dfc 100644 --- a/projects/app/src/pages/dashboard/evaluation/dimension/index.tsx +++ b/projects/app/src/pages/dashboard/evaluation/dimension/index.tsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react'; +import React, { useState, useMemo } from 'react'; import { Table, Thead, @@ -23,7 +23,6 @@ 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 Avatar from '@fastgpt/web/components/common/Avatar'; import { useTranslation } from 'next-i18next'; @@ -31,37 +30,61 @@ import EmptyTip from '@fastgpt/web/components/common/EmptyTip'; import { getMetricList, deleteMetric } from '@/web/core/evaluation/dimension'; import { EvalMetricTypeEnum } from '@fastgpt/global/core/evaluation/metric/constants'; import type { EvalMetricDisplayType } from '@fastgpt/global/core/evaluation/metric/type'; -import type { PaginationProps, PaginationResponse } from '@fastgpt/web/common/fetch/type'; +import { + getBuiltinDimensionInfo, + getBuiltinDimensionNameFromId +} from '@/web/core/evaluation/utils'; const EvaluationDimensions = ({ Tab }: { Tab: React.ReactNode }) => { const [searchValue, setSearchValue] = useState(''); + const [allDimensions, setAllDimensions] = useState([]); const { t } = useTranslation(); const router = useRouter(); - const getMetricListAdapter = async ( - data: PaginationProps<{ searchKey: string }> - ): Promise> => { - return getMetricList({ - pageNum: Number(data.pageNum), - pageSize: Number(data.pageSize), - searchKey: data.searchKey + const { runAsync: fetchAllDimensions, loading: isLoading } = useRequest2( + async () => { + const result = await getMetricList({}); + return result; + }, + { + manual: false, + onSuccess: (data) => { + setAllDimensions(data.list || []); + } + } + ); + + // 处理维度显示信息 + const processedDimensions = useMemo(() => { + return allDimensions.map((dimension) => { + // 如果是内置维度,使用国际化信息 + if (dimension.type === EvalMetricTypeEnum.Builtin) { + const dimensionName = getBuiltinDimensionNameFromId(dimension._id); + const builtinInfo = getBuiltinDimensionInfo(dimensionName); + if (builtinInfo) { + return { + ...dimension, + name: t(builtinInfo.name), + description: t(builtinInfo.description) + }; + } + } + return dimension; }); - }; + }, [allDimensions, t]); - const { - data: dimensions, - Pagination, - getData: fetchData, - isLoading, - total - } = usePagination<{ searchKey: string }, EvalMetricDisplayType>(getMetricListAdapter, { - defaultPageSize: 10, - params: { - searchKey: searchValue - }, - EmptyTip: , - refreshDeps: [searchValue] - }); + // 前端搜索过滤 + const filteredDimensions = useMemo(() => { + if (!searchValue.trim()) { + return processedDimensions; + } + return processedDimensions.filter( + (dimension) => + dimension.name.toLowerCase().includes(searchValue.toLowerCase()) || + (dimension.description && + dimension.description.toLowerCase().includes(searchValue.toLowerCase())) + ); + }, [processedDimensions, searchValue]); const { openConfirm, ConfirmModal } = useConfirm({ type: 'delete' @@ -69,7 +92,7 @@ const EvaluationDimensions = ({ Tab }: { Tab: React.ReactNode }) => { const { runAsync: onDeleteMetric } = useRequest2(deleteMetric, { onSuccess: () => { - fetchData(); + fetchAllDimensions(); }, errorToast: t('dashboard_evaluation:delete_failed'), successToast: t('dashboard_evaluation:delete_success') @@ -123,7 +146,7 @@ const EvaluationDimensions = ({ Tab }: { Tab: React.ReactNode }) => {
{dimension.description || '-'} - {format(new Date(dimension.createTime), 'yyyy-MM-dd HH:mm:ss')} - {format(new Date(dimension.updateTime), 'yyyy-MM-dd HH:mm:ss')} + {dimension.type === EvalMetricTypeEnum.Builtin ? ( + - + ) : ( + <> + {format(new Date(dimension.createTime), 'yyyy-MM-dd HH:mm:ss')} + {format(new Date(dimension.updateTime), 'yyyy-MM-dd HH:mm:ss')} + + )} - - - {dimension.sourceMember?.name} - + {dimension.sourceMember?.name ? ( + + + {dimension.sourceMember.name} + + ) : ( + - + )} e.stopPropagation()}> {dimension.type === EvalMetricTypeEnum.Custom && ( @@ -188,14 +221,12 @@ const EvaluationDimensions = ({ Tab }: { Tab: React.ReactNode }) => { ))}
- {total === 0 && } + {filteredDimensions.length === 0 && ( + + )} - - - - ); diff --git a/projects/app/src/pages/dashboard/evaluation/index.tsx b/projects/app/src/pages/dashboard/evaluation/index.tsx index 1961d8544ea7..a55f32df7a7b 100644 --- a/projects/app/src/pages/dashboard/evaluation/index.tsx +++ b/projects/app/src/pages/dashboard/evaluation/index.tsx @@ -59,7 +59,7 @@ export default Evaluation; export async function getServerSideProps(content: any) { return { props: { - ...(await serviceSideProps(content, ['dashboard_evaluation'])) + ...(await serviceSideProps(content, ['dashboard_evaluation', 'evaluation', 'dataset'])) } }; } diff --git a/projects/app/src/pages/dashboard/evaluation/task/detail/index.tsx b/projects/app/src/pages/dashboard/evaluation/task/detail/index.tsx index 7c686c702358..d72d5b041124 100644 --- a/projects/app/src/pages/dashboard/evaluation/task/detail/index.tsx +++ b/projects/app/src/pages/dashboard/evaluation/task/detail/index.tsx @@ -1,40 +1,1421 @@ -import React from 'react'; +'use client'; +import React, { useState, useMemo, useCallback } from 'react'; import { useRouter } from 'next/router'; -import { Box, Flex } from '@chakra-ui/react'; +import { + Box, + Flex, + Input, + InputGroup, + InputLeftElement, + HStack, + Button, + IconButton, + Textarea, + Switch, + Popover, + PopoverTrigger, + PopoverContent, + PopoverArrow, + useDisclosure, + Link +} from '@chakra-ui/react'; +import { useToast } from '@fastgpt/web/hooks/useToast'; +import { getErrText } from '@fastgpt/global/common/error/utils'; +import { serviceSideProps } from '@/web/common/i18n/utils'; import { useTranslation } from 'next-i18next'; -import FolderPath from '@/components/common/folder/Path'; +import { + TaskPageContext, + TaskPageContextProvider +} from '@/web/core/evaluation/context/taskPageContext'; +import { useContextSelector } from 'use-context-selector'; +import NextHead from '@/components/common/NextHead'; +import { useRequest2 } from '@fastgpt/web/hooks/useRequest'; +import { useScrollPagination } from '@fastgpt/web/hooks/useScrollPagination'; +import { useInterval } from 'ahooks'; +import NavBar, { + TabEnum, + getTabFilterParams +} from '@/pageComponents/dashboard/evaluation/task/detail/NavBar'; +import MyIcon from '@fastgpt/web/components/common/Icon'; +import { useForm } from 'react-hook-form'; +import PopoverConfirm from '@fastgpt/web/components/common/MyPopover/PopoverConfirm'; +import ScoreBar from '@/pageComponents/dashboard/evaluation/task/detail/ScoreBar'; +import ScoreDashboard from '@/pageComponents/dashboard/evaluation/task/detail/ScoreDashboard'; +import GradientBorderBox from '@/pageComponents/dashboard/evaluation/task/detail/GradientBorderBox'; +import EvaluationSummaryCard from '@/pageComponents/dashboard/evaluation/task/detail/EvaluationSummaryCard'; +import BasicInfo from '@/pageComponents/dashboard/evaluation/task/detail/BasicInfo'; +import ConfigParams from '@/pageComponents/dashboard/evaluation/task/detail/ConfigParams'; +import DetailedResponseModal from '@/pageComponents/dashboard/evaluation/task/detail/DetailedResponseModal'; +import MyBox from '@fastgpt/web/components/common/MyBox'; +import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip'; +import MyTooltip from '@fastgpt/web/components/common/MyTooltip'; +import EmptyTip from '@fastgpt/web/components/common/EmptyTip'; +import { getEvaluationItemList } from '@/web/core/evaluation/task'; +import { + BUILTIN_DIMENSION_MAP, + getBuiltinDimensionInfo, + formatScoreToPercentage, + getEvaluationErrorI18nKey +} from '@/web/core/evaluation/utils'; +import { + EvaluationStatusEnum, + EvaluationStatusMap +} from '@fastgpt/global/core/evaluation/constants'; +import { MetricResultStatusEnum } from '@fastgpt/global/core/evaluation/metric/constants'; +import { EvalDatasetDataKeyEnum } from '@fastgpt/global/core/evaluation/dataset/constants'; -const EvaluationTaskDetail = () => { +type Props = { taskId: string; currentTab: TabEnum }; + +const Detail = ({ taskId, currentTab }: Props) => { const { t } = useTranslation(); + const { toast } = useToast(); const router = useRouter(); - // 路径导航 - const paths = [{ parentId: 'current', parentName: 'taskName' }]; + // 从 Context 获取数据和方法 + const taskDetail = useContextSelector(TaskPageContext, (v) => v.taskDetail); + const loadTaskDetail = useContextSelector(TaskPageContext, (v) => v.loadTaskDetail); + const statsData = useContextSelector(TaskPageContext, (v) => v.statsData); + const summaryData = useContextSelector(TaskPageContext, (v) => v.summaryData); + const evaluationDetail = useContextSelector(TaskPageContext, (v) => v.evaluationDetail); + const loading = useContextSelector(TaskPageContext, (v) => v.loading); + const searchValue = useContextSelector(TaskPageContext, (v) => v.searchValue); + const setSearchValue = useContextSelector(TaskPageContext, (v) => v.setSearchValue); + const loadAllData = useContextSelector(TaskPageContext, (v) => v.loadAllData); + const deleteItem = useContextSelector(TaskPageContext, (v) => v.deleteItem); + const retryItem = useContextSelector(TaskPageContext, (v) => v.retryItem); + const updateItem = useContextSelector(TaskPageContext, (v) => v.updateItem); + const retryFailedItems = useContextSelector(TaskPageContext, (v) => v.retryFailedItems); + const exportItems = useContextSelector(TaskPageContext, (v) => v.exportItems); + const generateSummary = useContextSelector(TaskPageContext, (v) => v.generateSummary); + const generateSummaryForMetrics = useContextSelector( + TaskPageContext, + (v) => v.generateSummaryForMetrics + ); + + // 本地状态(UI 相关) + const [selectedIndex, setSelectedIndex] = useState(0); + const [editing, setEditing] = useState(false); + const [modifyDataset, setModifyDataset] = useState(true); + const [selectedDimension, setSelectedDimension] = useState(0); + const { + isOpen: isSavePopoverOpen, + onOpen: onSavePopoverOpen, + onClose: onSavePopoverClose + } = useDisclosure(); + const { + isOpen: isConfigParamsOpen, + onOpen: onConfigParamsOpen, + onClose: onConfigParamsClose + } = useDisclosure(); + const { + isOpen: isDetailedResponseOpen, + onOpen: onDetailedResponseOpen, + onClose: onDetailedResponseClose + } = useDisclosure(); + + // 判断是否处于排队中或评测中状态 + const isQueuingOrEvaluating = useMemo(() => { + if (!evaluationDetail?.status) return false; + + return ( + evaluationDetail.status === EvaluationStatusEnum.queuing || + evaluationDetail.status === EvaluationStatusEnum.evaluating + ); + }, [evaluationDetail]); + + // 判断是否全部数据执行异常 + const isAllDataFailed = useMemo(() => { + if (!evaluationDetail?.status || !statsData) return false; + + return ( + evaluationDetail.status === EvaluationStatusEnum.error && statsData.error === statsData.total + ); + }, [evaluationDetail, statsData]); + + // 判断是否为正常完成状态 + const isNormalCompleted = useMemo(() => { + if (!evaluationDetail?.status) return false; + + return ( + evaluationDetail.status === EvaluationStatusEnum.completed || + (evaluationDetail.status === EvaluationStatusEnum.error && !isAllDataFailed) + ); + }, [evaluationDetail, isAllDataFailed]); + + // 初始化数据加载 + useRequest2( + async () => { + const taskDetailData = await loadTaskDetail(taskId); + await loadAllData(taskDetailData); + }, + { + onError(err: any) { + router.replace(`/dashboard/evaluation/task/list`); + toast({ + title: t(getErrText(err, t('common:load_failed')) as any), + status: 'error' + }); + }, + manual: false + } + ); + + // 滚动分页参数 + const scrollParams = useMemo( + () => ({ + evalId: taskId, + ...(searchValue && { [EvalDatasetDataKeyEnum.UserInput]: searchValue }), + ...getTabFilterParams(currentTab) + }), + [taskId, searchValue, currentTab] + ); + + // 空状态提示组件 + const EmptyTipDom = useMemo( + () => , + [t] + ); + + // 使用滚动分页获取评估项列表 + const { + data: evaluationItems, + ScrollData, + total: totalItems, + refreshList: refreshEvaluationItems, + setData: setEvaluationItems, + fetchData: fetchEvaluationItems + } = useScrollPagination(getEvaluationItemList, { + pageSize: 20, + params: scrollParams, + refreshDeps: [searchValue, taskId, currentTab], + EmptyTip: EmptyTipDom, + errorToast: '' + }); + + // 智能轮询刷新函数 + const smartRefreshData = useCallback(async () => { + if (!taskId || evaluationItems.length === 0) return; + + try { + // 计算选中项所在的页码 + const selectedPageIndex = Math.floor(selectedIndex / 20); + + const pagesToLoad = selectedPageIndex + 1; + + let allData: any[] = []; + + // 逐页加载数据 + for (let page = 0; page < pagesToLoad; page++) { + const offset = page * 20; + const response = await getEvaluationItemList({ + ...scrollParams, + offset, + pageSize: 20 + }); + + allData = [...allData, ...response.list]; + + // 如果已经是最后一页,提前结束 + if (allData.length >= response.total) { + break; + } + } + + setEvaluationItems(allData); + + if (selectedIndex < allData.length) { + setSelectedIndex(selectedIndex); + } else if (allData.length > 0) { + setSelectedIndex(allData.length - 1); + } else { + setSelectedIndex(0); + } + } catch (error) { + console.error('Smart refresh error:', error); + // 发生错误时,使用原始的刷新方法作为兜底 + refreshEvaluationItems(); + } + }, [ + taskId, + evaluationItems, + selectedIndex, + scrollParams, + setEvaluationItems, + refreshEvaluationItems + ]); + + // 使用自定义轮询替代 useScrollPagination 的轮询 + const interval = useInterval(() => { + smartRefreshData(); + }, 15000); + + // 组件卸载时清除轮询 + React.useEffect(() => { + return () => { + interval?.(); + }; + }, [interval]); + + const handleSearch = useCallback( + (value: string) => { + setSearchValue(value); + // 重置选中项索引 + setSelectedIndex(0); + // 重置编辑状态 + setEditing(false); + }, + [setSearchValue] + ); + + // 计算序号格式 + const getItemNumber = useCallback( + (index: number) => { + const itemNumber = index + 1; + if (totalItems < 100) { + return itemNumber < 10 ? `0${itemNumber}` : `${itemNumber}`; + } else { + return itemNumber < 10 + ? `00${itemNumber}` + : itemNumber < 100 + ? `0${itemNumber}` + : `${itemNumber}`; + } + }, + [totalItems] + ); + + // 计算加权综合阈值 + const aggregateThreshold = useMemo(() => { + if (!summaryData?.data || summaryData.data.length === 0) { + return 0.8; // 默认阈值 + } + + // 计算加权综合阈值 + let totalWeightedThreshold = 0; + let totalWeight = 0; + + summaryData.data.forEach((item) => { + const weight = item.weight || 0; + const threshold = item.threshold || 0.8; + + totalWeightedThreshold += (threshold * weight) / 100; + totalWeight += weight; + }); + + // 如果总权重为0,使用平均阈值作为兜底 + if (totalWeight === 0) { + const totalThreshold = summaryData.data.reduce( + (sum, item) => sum + (item.threshold || 0.8), + 0 + ); + return totalThreshold / summaryData.data.length; + } + + return totalWeightedThreshold; + }, [summaryData]); + + // 动态计算表头 + const tableHeaders = useMemo(() => { + if (evaluationItems.length === 0) return []; + + const firstItem = evaluationItems[0]; + const evaluators = firstItem.evaluators || []; + + const headers = [ + { key: 'question', label: t('dashboard_evaluation:question_column'), flex: 3 } + ]; + + if (evaluators.length < 3) { + // 小于3个维度,显示每个维度名称 + evaluators.forEach((evaluator, index) => { + // 查找是否有匹配的内置维度信息 + const matchedDimension = + BUILTIN_DIMENSION_MAP[evaluator.metric.name as keyof typeof BUILTIN_DIMENSION_MAP]; + const displayName = matchedDimension ? t(matchedDimension.name) : evaluator.metric.name; + + headers.push({ + key: `metric_${index}`, + label: displayName, + flex: 1 + }); + }); + } else { + // 大于等于3个维度,只显示综合评分 + headers.push({ + key: 'totalScore', + label: t('dashboard_evaluation:comprehensive_score_column'), + flex: 1 + }); + } + + return headers; + }, [evaluationItems, t]); + + // 获取当前选中项的详细信息 + const selectedItem = useMemo(() => { + return evaluationItems[selectedIndex] || null; + }, [evaluationItems, selectedIndex]); + + // 根据选中项的 evaluatorOutputs 动态生成评测维度数据 + const evaluationDimensions = useMemo(() => { + if ( + !selectedItem || + selectedItem.status !== EvaluationStatusEnum.completed || + !selectedItem.evaluatorOutputs + ) { + return []; + } + + return selectedItem.evaluatorOutputs.map((output, index) => { + // 查找是否有匹配的内置维度信息 + const builtinInfo = getBuiltinDimensionInfo(output.metricName); + const displayName = builtinInfo ? t(builtinInfo.name) : output.metricName; + + // 从对应序号的 evaluators 中获取阈值 + const evaluator = selectedItem.evaluators?.[index]; + const threshold = evaluator?.thresholdValue || 0.8; // 默认阈值为 0.8 + + return { + name: displayName, + score: formatScoreToPercentage(output.data?.score || 0), + threshold: formatScoreToPercentage(threshold), + description: output.data?.reason || '-' + }; + }); + }, [selectedItem, t]); + + // 获取错误信息列表 + const errorMessages = useMemo(() => { + if (!selectedItem || selectedItem.status !== EvaluationStatusEnum.error) { + return []; + } + + const messages: string[] = []; + + // 优先取 evaluatorOutputs 下的 data.reason,但需要 status 为 Failed + if (selectedItem.evaluatorOutputs && selectedItem.evaluatorOutputs.length > 0) { + selectedItem.evaluatorOutputs.forEach((output) => { + if (output.status === MetricResultStatusEnum.Failed && output.data?.reason) { + // Try to translate error message from evaluator output + const i18nKey = getEvaluationErrorI18nKey(output.data.reason); + const translatedMessage = t(i18nKey, output.data.reason); // Use original as fallback + messages.push(translatedMessage); + } + }); + } + + // 如果没有找到任何 reason,则取外层的 errorMessage 作为兜底 + if (messages.length === 0 && selectedItem.errorMessage) { + // Try to translate the main error message + const i18nKey = getEvaluationErrorI18nKey(selectedItem.errorMessage); + const translatedMessage = t(i18nKey, selectedItem.errorMessage); // Use original as fallback + messages.push(translatedMessage); + } + + return messages; + }, [selectedItem, t]); + + const { register, handleSubmit, reset, setValue } = useForm(); + + // 添加一个重置表单的函数 + const resetForm = useCallback(() => { + reset(); + }, [reset]); + + const handleEdit = useCallback(() => { + // 重置表单并设置当前选中项的值 + if (selectedItem) { + reset({ + question: selectedItem.dataItem.userInput, + expectedResponse: selectedItem.dataItem.expectedOutput + }); + } + setEditing(true); + }, [selectedItem, reset]); + + const handleSave = useCallback( + async (data: any) => { + if (!selectedItem) { + return; + } + + try { + const updateData = { + evalItemId: selectedItem._id, + [EvalDatasetDataKeyEnum.UserInput]: data.question, + [EvalDatasetDataKeyEnum.ExpectedOutput]: data.expectedResponse, + modifyDataset: modifyDataset + }; + + await updateItem(selectedItem._id, updateData); + // 刷新列表数据 + await refreshEvaluationItems(); + setEditing(false); + onSavePopoverClose(); + } catch (error: any) { + // 错误处理已在 Context 中完成 + console.error('save error:', error); + } + }, + [selectedItem, modifyDataset, onSavePopoverClose, updateItem, refreshEvaluationItems] + ); + + const handleRefresh = useCallback(async () => { + if (!selectedItem) { + return; + } + + try { + await retryItem(selectedItem._id); + // 刷新列表数据 + await refreshEvaluationItems(); + } catch (error: any) { + // 错误处理已在 Context 中完成 + console.error('retry error:', error); + } + }, [selectedItem, retryItem, refreshEvaluationItems]); + + const handleDelete = useCallback(async () => { + if (!selectedItem) { + return; + } + + try { + await deleteItem(selectedItem._id); + + // 刷新列表数据 + await refreshEvaluationItems(); + + // 调整选中索引:选择下一个项目,如果是最后一项则选择新列表的最后一项 + const currentItemIndex = selectedIndex; + const newSelectedIndex = + currentItemIndex < evaluationItems.length - 1 + ? currentItemIndex // 选择下一个项目(当前索引保持不变,因为删除后后面的项目会前移) + : Math.max(0, evaluationItems.length - 2); // 如果是最后一项,选择删除后列表的最后一项 + + setSelectedIndex(newSelectedIndex); + + // 重置编辑状态 + setEditing(false); + resetForm(); + } catch (error: any) { + // 错误处理已在 Context 中完成 + console.error('delete error:', error); + } + }, [selectedItem, evaluationItems, selectedIndex, resetForm, deleteItem, refreshEvaluationItems]); + + const handleCancel = useCallback(() => { + setEditing(false); + // 取消时重置表单 + resetForm(); + }, [resetForm]); + + // 导出数据处理函数 + const handleExport = useCallback(async () => { + try { + await exportItems('csv'); + } catch (error: any) { + // 错误处理已在 Context 中完成 + console.error('export error:', error); + } + }, [exportItems]); + + // 重试失败项处理函数 + const handleRetryFailed = useCallback(async () => { + try { + await retryFailedItems(); + // 切换到全部数据标签页,useScrollPagination 会自动响应 currentTab 变化并刷新列表 + router.replace({ + query: { + taskId: taskId, + currentTab: TabEnum.allData + } + }); + } catch (error: any) { + // 错误处理已在 Context 中完成 + console.error('retry error:', error); + } + }, [retryFailedItems, router, taskId]); + + // 刷新评分处理函数 + const handleRefreshScore = useCallback(async () => { + try { + await generateSummary(); + } catch (error: any) { + // 错误处理已在 Context 中完成 + console.error('refresh error', error); + } + }, [generateSummary]); + + // 设置评分处理函数 + const handleScoreSettings = useCallback(() => { + onConfigParamsOpen(); + }, [onConfigParamsOpen]); + + // 查看完整响应处理函数 + const handleViewFullResponse = useCallback(() => { + if (!selectedItem) { + return; + } + + // 检查是否有必要的数据 + if (!selectedItem.targetOutput?.aiChatItemDataId) { + toast({ + title: 'dataId is Required', + status: 'warning' + }); + return; + } + + onDetailedResponseOpen(); + }, [selectedItem, toast, onDetailedResponseOpen]); + + // 处理配置参数确认 + const handleConfigParamsConfirm = useCallback(() => { + // 配置保存成功后刷新相关数据 + loadAllData(taskDetail); + }, [loadAllData, taskDetail]); return ( - - {/* 顶部导航栏 */} - - {/* 路径导航 */} - - { - router.push(`/dashboard/evaluation?evaluationTab=tasks`); - }} + <> + + + + {/* 左侧主内容区域 */} + + {/* 顶部 NavBar */} + + + {/* 数据和详情的水平布局 */} + + {/* 数据区域 - 占 4/10 */} + + {/* 数据区域头部 */} + + + + + {t('dashboard_evaluation:data_with_count', { + data: statsData + ? statsData.completed !== statsData.total + ? `${statsData.completed}/${statsData.total}` + : statsData.total + : 0 + })} + + + + + + + + handleSearch(e.target.value)} + bg={'white'} + /> + + + + {/* 数据列表内容区域 */} + + {/* 表头 - 只在有数据且不在加载中时显示 */} + {tableHeaders.length > 0 && !loading.items && ( + + {tableHeaders.map((header, index) => ( + + {header.label} + + ))} + + )} + + {/* 数据列表 - 使用滚动分页组件 */} + + {evaluationItems.map((item, index) => { + const evaluatorOutputs = item.evaluatorOutputs || []; + + return ( + { + setSelectedIndex(index); + setEditing(false); + // 切换选中项时重置表单 + resetForm(); + }} + > + {/* 问题列 */} + + + {getItemNumber(index)} + + {item.dataItem.userInput} + + + + + {/* 动态列 */} + {evaluatorOutputs.length === 0 ? ( + // 当 evaluatorOutputs 为空时,根据评估器数量显示对应的状态列 + (item.evaluators || []).length < 3 ? ( + // 显示每个评估器对应的状态列 + (item.evaluators || []).map((evaluator, evaluatorIndex) => { + // 查找是否有匹配的内置维度信息 + const builtinInfo = getBuiltinDimensionInfo(evaluator.metric.name); + const displayName = builtinInfo + ? t(builtinInfo.name) + : evaluator.metric.name; + + return ( + { + if (item.status === EvaluationStatusEnum.evaluating) { + return 'blue.500'; + } else if (item.status === EvaluationStatusEnum.error) { + return 'red.500'; + } + return 'myGray.600'; + })()} + > + {(() => { + const statusInfo = EvaluationStatusMap[item.status]; + return statusInfo ? t(statusInfo.name) : '-'; + })()} + + ); + }) + ) : ( + // 显示综合评分状态 + { + if (item.status === EvaluationStatusEnum.evaluating) { + return 'blue.500'; + } else if (item.status === EvaluationStatusEnum.error) { + return 'red.500'; + } + return 'myGray.600'; + })()} + > + {(() => { + const statusInfo = EvaluationStatusMap[item.status]; + return statusInfo ? t(statusInfo.name) : '-'; + })()} + + ) + ) : evaluatorOutputs.length < 3 ? ( + // 显示每个维度的分数或状态 + evaluatorOutputs.map((output, outputIndex) => { + // 获取显示内容和颜色 + const getDisplayInfo = () => { + if ( + output.status === MetricResultStatusEnum.Success && + output.data?.score !== undefined + ) { + // 获取对应序号的评估器阈值 + const evaluator = item.evaluators?.[outputIndex]; + const threshold = evaluator?.thresholdValue || 0.8; + const score = output.data.score; + + // 比较得分与阈值,决定颜色 + const color = score >= threshold ? 'myGray.600' : 'yellow.600'; + + return { + content: formatScoreToPercentage(score), + color: color + }; + } + + if (output.status === MetricResultStatusEnum.Failed) { + return { + content: t('dashboard_evaluation:abnormal_status'), + color: 'red.500' + }; + } + + // 使用外层状态 + const statusInfo = EvaluationStatusMap[item.status]; + const statusName = statusInfo ? t(statusInfo.name) : '-'; + + let color = 'myGray.600'; // 默认颜色(排队中) + if (item.status === EvaluationStatusEnum.evaluating) { + color = 'blue.500'; // 评测中为蓝色 + } else if (item.status === EvaluationStatusEnum.error) { + color = 'red.500'; // 异常为红色 + } + + return { + content: statusName, + color: color + }; + }; + + const displayInfo = getDisplayInfo(); + + return ( + + {displayInfo.content} + + ); + }) + ) : ( + // 显示综合评分或状态(计算加权综合得分) + { + // 只有当外层状态为 completed 时才计算综合得分的颜色 + if (item.status === EvaluationStatusEnum.completed) { + const hasFailedOutputs = evaluatorOutputs.some( + (output) => output.status === MetricResultStatusEnum.Failed + ); + const successOutputs = evaluatorOutputs.filter( + (output) => + output.status === MetricResultStatusEnum.Success && + output.data?.score !== undefined + ); + + if (hasFailedOutputs) { + return 'red.500'; + } + + if (successOutputs.length > 0) { + // 计算综合得分和综合阈值 + let totalWeightedScore = 0; + let totalWeightedThreshold = 0; + let totalWeight = 0; + + evaluatorOutputs.forEach((output, outputIndex) => { + if ( + output.status === MetricResultStatusEnum.Success && + output.data?.score !== undefined + ) { + const evaluator = item.evaluators?.[outputIndex]; + const weight = evaluator?.weight || 0; + const score = output.data.score; + const threshold = evaluator?.thresholdValue || 0.8; + + totalWeightedScore += (score * weight) / 100; + totalWeightedThreshold += (threshold * weight) / 100; + totalWeight += weight; + } + }); + + // 比较综合得分与综合阈值 + if (totalWeight > 0) { + return totalWeightedScore >= totalWeightedThreshold + ? 'myGray.600' + : 'yellow.600'; + } + + return 'myGray.600'; + } + } + + // 使用外层状态确定颜色 + if (item.status === EvaluationStatusEnum.evaluating) { + return 'blue.500'; + } else if (item.status === EvaluationStatusEnum.error) { + return 'red.500'; + } + + return 'myGray.600'; + })()} + > + {(() => { + // 只有当外层状态为 completed 时才显示加权综合得分 + if (item.status === EvaluationStatusEnum.completed) { + const successOutputs = evaluatorOutputs.filter( + (output) => + output.status === MetricResultStatusEnum.Success && + output.data?.score !== undefined + ); + const failedOutputs = evaluatorOutputs.filter( + (output) => output.status === MetricResultStatusEnum.Failed + ); + + if (failedOutputs.length > 0) { + return t('dashboard_evaluation:abnormal_status'); + } + + if (successOutputs.length > 0) { + // 计算加权综合得分 + let totalWeightedScore = 0; + let totalWeight = 0; + + evaluatorOutputs.forEach((output, outputIndex) => { + if ( + output.status === MetricResultStatusEnum.Success && + output.data?.score !== undefined + ) { + const evaluator = item.evaluators?.[outputIndex]; + const weight = evaluator?.weight || 0; + const score = output.data.score; + + totalWeightedScore += (score * weight) / 100; + totalWeight += weight; + } + }); + + if (totalWeight > 0) { + return formatScoreToPercentage(totalWeightedScore); + } + + // 如果没有权重信息,使用平均分作为兜底 + const totalScore = successOutputs.reduce( + (sum, output) => sum + (output.data?.score || 0), + 0 + ); + const avgScore = totalScore / successOutputs.length; + return formatScoreToPercentage(avgScore); + } + } + + // 使用外层状态 + const statusInfo = EvaluationStatusMap[item.status]; + return statusInfo ? t(statusInfo.name) : '-'; + })()} + + )} + + ); + })} + + + + + {/* 详情区域 - 占 6/10 */} + + {/* 详情区域头部 */} + + + + + {t('dashboard_evaluation:detail_title')} + + + + + {editing ? ( + <> + + + + + + + + + + {t('dashboard_evaluation:modify_dataset_simultaneously')} + + setModifyDataset(e.target.checked)} + colorScheme="primary" + /> + + + + + + + + + ) : ( + <> + {/* 根据选中项状态动态显示按钮 */} + {selectedItem?.status === EvaluationStatusEnum.error && ( + <> + + } + onClick={handleRefresh} + /> + + + } + onClick={handleEdit} + /> + + } + /> + } + /> + + )} + + {selectedItem?.status === EvaluationStatusEnum.completed && ( + <> + + } + /> + } + /> + + )} + + {/* 排队中或评测中状态不显示任何按钮 */} + + )} + + + + {/* 详情内容区域 */} + + {/* 评测维度组件 - 仅在选中项状态为 completed 且有 evaluatorOutputs 时显示 */} + {selectedItem && + selectedItem.status === EvaluationStatusEnum.completed && + evaluationDimensions.length > 0 && ( + + + {evaluationDimensions.map((dimension, index) => { + const isActive = selectedDimension === index; + const isAboveThreshold = dimension.score >= dimension.threshold; + const bgColor = isAboveThreshold ? 'blue.50' : 'yellow.50'; + const borderColor = isAboveThreshold ? 'blue.200' : 'yellow.200'; + const textColor = isAboveThreshold ? 'blue.600' : 'yellow.600'; + + return ( + setSelectedDimension(index)} + > + {dimension.name} + {dimension.score} + + ); + })} + + + {evaluationDimensions[selectedDimension] && ( + = + evaluationDimensions[selectedDimension].threshold + ? 'blue.600' + : 'yellow.600' + } + fontSize={'sm'} + lineHeight={'1.6'} + mt={2} + > + {evaluationDimensions[selectedDimension].description} + + )} + + )} + + {/* 根据选中的索引显示对应的详情内容 */} + {selectedItem && ( + <> + {/* 错误信息显示 */} + {errorMessages.length > 0 && ( + + + + + {errorMessages.map((message, index) => ( + + {index + 1}、{message} + + ))} + + + + )} + + + {t('dashboard_evaluation:question_field')} + {editing ? ( +