diff --git a/docSite/content/zh-cn/docs/development/upgrading/4101.md b/docSite/content/zh-cn/docs/development/upgrading/4101.md index 7058a552a..72dfef938 100644 --- a/docSite/content/zh-cn/docs/development/upgrading/4101.md +++ b/docSite/content/zh-cn/docs/development/upgrading/4101.md @@ -7,16 +7,48 @@ toc: true weight: 784 --- +## 更新指南 -## 🚀 新增内容 +### 1. 更新镜像: +- 更新 FastGPT 镜像 +- 更新 FastGPT 商业版镜像 +- 更新 fastgpt-plugin 镜像 +- mcp_server 无需更新 +- Sandbox 无需更新 +- AIProxy 无需更新 + +### 2. 执行升级脚本 + +该脚本仅需商业版用户执行。 + +从任意终端,发起 1 个 HTTP 请求。其中 {{rootkey}} 替换成环境变量里的 `rootkey`;{{host}} 替换成**FastGPT 域名**。 + +```bash +curl --location --request POST 'https://{{host}}/api/admin/initv4101' \ +--header 'rootkey: {{rootkey}}' \ +--header 'Content-Type: application/json' +``` + +- 给自动同步的知识库加入新的定时任务。 ## ⚙️ 优化 -1. 定时任务报错日志记录到对话日志 +1. 定时任务报错日志记录到对话日志。 +2. 封装应用动态form渲染组件。 +3. 商业版第三方知识库定时同步,支持全量同步,可以同步整个目录。 +4. 目录面包屑导航溢出省略。 ## 🐛 修复 -1. 搜索类型系统工具无法正常显示 -2. 部分系统工具向下兼容问题 +1. 搜索类型系统工具无法正常显示。 +2. 部分系统工具向下兼容问题。 3. AI 节点,手动选择历史记录时,会导致 system 记录重复。 +4. 知识库 tag 无法滚动到底。 + +## 🔨 工具更新 + +1. 新增 Flux 官方绘图工具。 +2. 新增 JinaAI 工具集。 +3. 新增阿里百炼 Flux 和通义万相绘图。 +4. 纠正硅基流动画图工具输出值类型。 \ No newline at end of file diff --git a/docSite/content/zh-cn/docs/development/upgrading/4913.md b/docSite/content/zh-cn/docs/development/upgrading/4913.md index dd51fa60b..e2c875b4f 100644 --- a/docSite/content/zh-cn/docs/development/upgrading/4913.md +++ b/docSite/content/zh-cn/docs/development/upgrading/4913.md @@ -32,4 +32,4 @@ weight: 787 1. 对话日志,日期范围选择问题。 2. API 调用时,传入的 system 提示词可能会重复。 3. AI 对话/工具调用,未选择文件链接时,也会从历史记录读取文件。 -4. 手动更新知识库索引时,错误的删除旧索引,导致手动索引无效。 \ No newline at end of file +4. 手动更新知识库索引时,错误的删除旧索引,导致手动索引无效。 diff --git a/docSite/content/zh-cn/docs/guide/knowledge_base/api_dataset.md b/docSite/content/zh-cn/docs/guide/knowledge_base/api_dataset.md index 8670c0c2b..fb3211c32 100644 --- a/docSite/content/zh-cn/docs/guide/knowledge_base/api_dataset.md +++ b/docSite/content/zh-cn/docs/guide/knowledge_base/api_dataset.md @@ -46,6 +46,7 @@ type FileListItem = { parentId: string //也可能为 null 或者 undefined 类型; name: string; type: 'file' | 'folder'; + hasChild?: boolean; // 是否有子文档(folder 强制为 true) updateTime: Date; createTime: Date; } @@ -89,6 +90,7 @@ curl --location --request POST '{{baseURL}}/v1/file/list' \ "id": "xxxx", "parentId": "xxxx", "type": "file", // file | folder + "hasChild": true, // 是否有子文档(folder 会强制为 true) "name":"test.json", "updateTime":"2024-11-26T03:05:24.759Z", "createTime":"2024-11-26T03:05:24.759Z" @@ -210,9 +212,13 @@ curl --location --request GET '{{baseURL}}/v1/file/detail?id=xx' \ "success": true, "message": "", "data": { - "id": "docs", - "parentId": "", - "name": "docs" + "id": "xxxx", + "parentId": "xxxx", + "type": "file", // file | folder + "hasChild": true, // 是否有子文档(folder 会强制为 true) + "name":"test.json", + "updateTime":"2024-11-26T03:05:24.759Z", + "createTime":"2024-11-26T03:05:24.759Z" } } ``` diff --git a/docSite/content/zh-cn/docs/guide/knowledge_base/third_dataset.md b/docSite/content/zh-cn/docs/guide/knowledge_base/third_dataset.md index f65e4b6fa..a5496046f 100644 --- a/docSite/content/zh-cn/docs/guide/knowledge_base/third_dataset.md +++ b/docSite/content/zh-cn/docs/guide/knowledge_base/third_dataset.md @@ -45,14 +45,15 @@ export type YuqueServer = { ### 2. 创建 Hook 文件 -每个第三方文档库都会采用 Hook 的方式来实现一套 API 接口的维护,Hook 里包含 4 个函数需要完成。 +每个第三方文档库都会采用 Hook 的方式来实现一套 API 接口的维护,Hook 里包含 5 个函数需要完成。 - 在`FastGPT\packages\service\core\dataset\apiDataset\`下创建一个文档库的文件夹,然后在文件夹下创建一个`api.ts`文件 -- 在`api.ts`文件中,需要完成 4 个函数的定义,分别是: +- 在`api.ts`文件中,需要完成 5 个函数的定义,分别是: - `listFiles`:获取文件列表 - `getFileContent`:获取文件内容/文件链接 - `getFileDetail`:获取文件详情信息 - `getFilePreviewUrl`:获取原文预览地址 + - `getFileId`: 获取原文件真实Id ### 3. 添加知识库类型 diff --git a/packages/global/common/string/time.ts b/packages/global/common/string/time.ts index 87b612fd3..0689803f5 100644 --- a/packages/global/common/string/time.ts +++ b/packages/global/common/string/time.ts @@ -97,8 +97,10 @@ export const getNextTimeByCronStringAndTimezone = ({ }; const interval = cronParser.parseExpression(cronString, options); const date = interval.next().toString(); + return new Date(date); } catch (error) { - return new Date('2099'); + console.log('getNextTimeByCronStringAndTimezone error', error); + return new Date(); } }; diff --git a/packages/global/core/app/jsonschema.ts b/packages/global/core/app/jsonschema.ts index 14c9a7b26..43326d909 100644 --- a/packages/global/core/app/jsonschema.ts +++ b/packages/global/core/app/jsonschema.ts @@ -17,7 +17,7 @@ export type JSONSchemaInputType = { required?: string[]; }; -const getNodeInputTypeFromSchemaInputType = ({ +export const getNodeInputTypeFromSchemaInputType = ({ type, arrayItems }: { diff --git a/packages/global/core/dataset/api.d.ts b/packages/global/core/dataset/api.d.ts index 7dda0f15e..1a3935127 100644 --- a/packages/global/core/dataset/api.d.ts +++ b/packages/global/core/dataset/api.d.ts @@ -13,6 +13,7 @@ import type { ParagraphChunkAIModeEnum } from './constants'; import type { ParentIdType } from '../../common/parentFolder/type'; +import type { APIFileItemType } from './apiDataset/type'; /* ================= dataset ===================== */ export type DatasetUpdateBody = { @@ -57,6 +58,7 @@ export type CreateDatasetCollectionParams = DatasetCollectionStoreDataType & { externalFileId?: string; externalFileUrl?: string; apiFileId?: string; + apiFileParentId?: string; //when file is imported by folder, the parentId is the folderId rawTextLength?: number; hashRawText?: string; @@ -65,7 +67,6 @@ export type CreateDatasetCollectionParams = DatasetCollectionStoreDataType & { createTime?: Date; updateTime?: Date; - nextSyncTime?: Date; }; export type ApiCreateDatasetCollectionParams = DatasetCollectionStoreDataType & { @@ -83,6 +84,9 @@ export type ApiDatasetCreateDatasetCollectionParams = ApiCreateDatasetCollection name: string; apiFileId: string; }; +export type ApiDatasetCreateDatasetCollectionV2Params = ApiCreateDatasetCollectionParams & { + apiFiles: APIFileItemType[]; +}; export type FileIdCreateDatasetCollectionParams = ApiCreateDatasetCollectionParams & { fileId: string; }; @@ -139,7 +143,7 @@ export type PushDatasetDataChunkProps = { indexes?: Omit[]; }; -export type PostWebsiteSyncParams = { +export type PostDatasetSyncParams = { datasetId: string; }; diff --git a/packages/global/core/dataset/apiDataset/type.d.ts b/packages/global/core/dataset/apiDataset/type.d.ts index af3c94ac4..1e8758411 100644 --- a/packages/global/core/dataset/apiDataset/type.d.ts +++ b/packages/global/core/dataset/apiDataset/type.d.ts @@ -1,8 +1,9 @@ import { RequireOnlyOne } from '../../../common/type/utils'; import type { ParentIdType } from '../../../common/parentFolder/type'; -export type APIFileItem = { +export type APIFileItemType = { id: string; + rawId: string; parentId: ParentIdType; name: string; type: 'file' | 'folder'; @@ -36,8 +37,6 @@ export type ApiDatasetServerType = { // Api dataset api -export type APIFileListResponse = APIFileItem[]; - export type ApiFileReadContentResponse = { title?: string; rawText: string; @@ -47,8 +46,4 @@ export type APIFileReadResponse = { url: string; }; -export type ApiDatasetDetailResponse = { - id: string; - name: string; - parentId: ParentIdType; -}; +export type ApiDatasetDetailResponse = APIFileItemType; diff --git a/packages/global/core/dataset/collection/constants.ts b/packages/global/core/dataset/collection/constants.ts index b26e3ea73..c3c87f3ca 100644 --- a/packages/global/core/dataset/collection/constants.ts +++ b/packages/global/core/dataset/collection/constants.ts @@ -4,3 +4,5 @@ export enum CollectionSourcePrefixEnum { link = 'link', external = 'external' } + +export const RootCollectionId = 'SYSTEM_ROOT'; diff --git a/packages/global/core/dataset/type.d.ts b/packages/global/core/dataset/type.d.ts index 65030f76e..26df8a530 100644 --- a/packages/global/core/dataset/type.d.ts +++ b/packages/global/core/dataset/type.d.ts @@ -106,13 +106,13 @@ export type DatasetCollectionSchemaType = ChunkSettingsType & { // Status forbid?: boolean; - nextSyncTime?: Date; // Collection metadata fileId?: string; // local file id rawLink?: string; // link url externalFileId?: string; //external file id apiFileId?: string; // api file id + apiFileParentId?: string; externalFileUrl?: string; // external import url rawTextLength?: number; diff --git a/packages/service/common/bullmq/index.ts b/packages/service/common/bullmq/index.ts index b7881e353..4bdc02a1c 100644 --- a/packages/service/common/bullmq/index.ts +++ b/packages/service/common/bullmq/index.ts @@ -19,6 +19,8 @@ const defaultWorkerOpts: Omit = { }; export enum QueueNames { + datasetSync = 'datasetSync', + // abondoned websiteSync = 'websiteSync' } diff --git a/packages/service/core/dataset/apiDataset/custom/api.ts b/packages/service/core/dataset/apiDataset/custom/api.ts index eb98ecebb..277ca4eb9 100644 --- a/packages/service/core/dataset/apiDataset/custom/api.ts +++ b/packages/service/core/dataset/apiDataset/custom/api.ts @@ -1,5 +1,4 @@ import type { - APIFileListResponse, ApiFileReadContentResponse, APIFileReadResponse, ApiDatasetDetailResponse, @@ -19,6 +18,16 @@ type ResponseDataType = { data: any; }; +type APIFileListResponse = { + id: string; + parentId: ParentIdType; + name: string; + type: 'file' | 'folder'; + updateTime: Date; + createTime: Date; + hasChild?: boolean; +}; + export const useApiDatasetRequest = ({ apiServer }: { apiServer: APIFileServer }) => { const instance = axios.create({ baseURL: apiServer.baseUrl, @@ -106,6 +115,7 @@ export const useApiDatasetRequest = ({ apiServer }: { apiServer: APIFileServer } const formattedFiles = files.map((file) => ({ ...file, + rawId: file.id, hasChild: file.hasChild ?? file.type === 'folder' })); @@ -201,18 +211,27 @@ export const useApiDatasetRequest = ({ apiServer }: { apiServer: APIFileServer } if (fileData) { return { id: fileData.id, + rawId: apiFileId, name: fileData.name, - parentId: fileData.parentId === null ? '' : fileData.parentId + parentId: fileData.parentId === null ? '' : fileData.parentId, + type: fileData.type, + updateTime: fileData.updateTime, + createTime: fileData.createTime }; } return Promise.reject('File not found'); }; + const getFileRawId = (fileId: string) => { + return fileId; + }; + return { getFileContent, listFiles, getFilePreviewUrl, - getFileDetail + getFileDetail, + getFileRawId }; }; diff --git a/packages/service/core/dataset/apiDataset/feishuDataset/api.ts b/packages/service/core/dataset/apiDataset/feishuDataset/api.ts index 61146e663..9c840e62b 100644 --- a/packages/service/core/dataset/apiDataset/feishuDataset/api.ts +++ b/packages/service/core/dataset/apiDataset/feishuDataset/api.ts @@ -1,5 +1,5 @@ import type { - APIFileItem, + APIFileItemType, ApiFileReadContentResponse, ApiDatasetDetailResponse, FeishuServer @@ -104,7 +104,11 @@ export const useFeishuDatasetRequest = ({ feishuServer }: { feishuServer: Feishu .catch((err) => responseError(err)); }; - const listFiles = async ({ parentId }: { parentId?: ParentIdType }): Promise => { + const listFiles = async ({ + parentId + }: { + parentId?: ParentIdType; + }): Promise => { const fetchFiles = async (pageToken?: string): Promise => { const data = await request( `/open-apis/drive/v1/files`, @@ -130,6 +134,7 @@ export const useFeishuDatasetRequest = ({ feishuServer }: { feishuServer: Feishu .filter((file) => ['folder', 'docx'].includes(file.type)) .map((file) => ({ id: file.token, + rawId: file.token, parentId: file.parent_token, name: file.name, type: file.type === 'folder' ? ('folder' as const) : ('file' as const), @@ -186,23 +191,33 @@ export const useFeishuDatasetRequest = ({ feishuServer }: { feishuServer: Feishu }: { apiFileId: string; }): Promise => { - const { document } = await request<{ document: { title: string } }>( + const { document } = await request<{ document: { title: string; type: string } }>( `/open-apis/docx/v1/documents/${apiFileId}`, {}, 'GET' ); return { + rawId: apiFileId, name: document?.title, parentId: null, - id: apiFileId + id: apiFileId, + type: document.type === 'folder' ? ('folder' as const) : ('file' as const), + hasChild: document.type === 'folder', + updateTime: new Date(), + createTime: new Date() }; }; + const getFileRawId = (fileId: string) => { + return fileId; + }; + return { getFileContent, listFiles, getFilePreviewUrl, - getFileDetail + getFileDetail, + getFileRawId }; }; diff --git a/packages/service/core/dataset/apiDataset/yuqueDataset/api.ts b/packages/service/core/dataset/apiDataset/yuqueDataset/api.ts index 9ddcaf4be..7ce16e0b2 100644 --- a/packages/service/core/dataset/apiDataset/yuqueDataset/api.ts +++ b/packages/service/core/dataset/apiDataset/yuqueDataset/api.ts @@ -1,5 +1,5 @@ import type { - APIFileItem, + APIFileItemType, ApiFileReadContentResponse, YuqueServer, ApiDatasetDetailResponse @@ -106,7 +106,7 @@ export const useYuqueDatasetRequest = ({ yuqueServer }: { yuqueServer: YuqueServ if (yuqueServer.basePath) parentId = yuqueServer.basePath; } - let files: APIFileItem[] = []; + let files: APIFileItemType[] = []; if (!parentId) { const limit = 100; @@ -133,7 +133,8 @@ export const useYuqueDatasetRequest = ({ yuqueServer }: { yuqueServer: YuqueServ files = allData.map((item) => { return { - id: item.id, + id: String(item.id), + rawId: String(item.id), name: item.name, parentId: null, type: 'folder', @@ -144,7 +145,8 @@ export const useYuqueDatasetRequest = ({ yuqueServer }: { yuqueServer: YuqueServ }; }); } else { - if (typeof parentId === 'number') { + const numParentId = Number(parentId); + if (!isNaN(numParentId)) { const data = await request( `/api/v2/repos/${parentId}/toc`, {}, @@ -155,6 +157,7 @@ export const useYuqueDatasetRequest = ({ yuqueServer }: { yuqueServer: YuqueServ .filter((item) => !item.parent_uuid && item.type !== 'LINK') .map((item) => ({ id: `${parentId}-${item.id}-${item.uuid}`, + rawId: String(item.uuid), name: item.title, parentId: item.parent_uuid, type: item.type === 'TITLE' ? ('folder' as const) : ('file' as const), @@ -167,11 +170,11 @@ export const useYuqueDatasetRequest = ({ yuqueServer }: { yuqueServer: YuqueServ } else { const [repoId, uuid, parentUuid] = parentId.split(/-(.*?)-(.*)/); const data = await request(`/api/v2/repos/${repoId}/toc`, {}, 'GET'); - return data .filter((item) => item.parent_uuid === parentUuid) .map((item) => ({ id: `${repoId}-${item.id}-${item.uuid}`, + rawId: String(item.uuid), name: item.title, parentId: item.parent_uuid, type: item.type === 'TITLE' ? ('folder' as const) : ('file' as const), @@ -207,6 +210,10 @@ export const useYuqueDatasetRequest = ({ yuqueServer }: { yuqueServer: YuqueServ 'GET' ); + if (!data.title) { + return Promise.reject('Cannot find the file'); + } + return { title: data.title, rawText: data.body @@ -266,8 +273,13 @@ export const useYuqueDatasetRequest = ({ yuqueServer }: { yuqueServer: YuqueServ } return { id: file.id, + rawId: file.id, name: file.name, - parentId: null + parentId: null, + type: file.type === 'TITLE' ? ('folder' as const) : ('file' as const), + updateTime: file.updated_at, + createTime: file.created_at, + hasChild: true }; } else { const [repoId, parentUuid, fileId] = apiFileId.split(/-(.*?)-(.*)/); @@ -283,23 +295,43 @@ export const useYuqueDatasetRequest = ({ yuqueServer }: { yuqueServer: YuqueServ if (file.parent_uuid) { return { id: file.id, + rawId: file.id, name: file.title, - parentId: parentId + parentId: parentId, + type: file.type === 'TITLE' ? ('folder' as const) : ('file' as const), + updateTime: new Date(), + createTime: new Date(), + hasChild: !!file.child_uuid }; } else { return { id: file.id, + rawId: file.id, name: file.title, - parentId: repoId + parentId: repoId, + type: file.type === 'TITLE' ? ('folder' as const) : ('file' as const), + updateTime: new Date(), + createTime: new Date(), + hasChild: !!file.child_uuid }; } } }; + const getFileRawId = (fileId: string) => { + const [repoId, parentUuid, fileUuid] = fileId.split(/-(.*?)-(.*)/); + if (fileUuid) { + return `${fileUuid}`; + } else { + return `${repoId}`; + } + }; + return { getFileContent, listFiles, getFilePreviewUrl, - getFileDetail + getFileDetail, + getFileRawId }; }; diff --git a/packages/service/core/dataset/collection/controller.ts b/packages/service/core/dataset/collection/controller.ts index 07605516e..3e0231005 100644 --- a/packages/service/core/dataset/collection/controller.ts +++ b/packages/service/core/dataset/collection/controller.ts @@ -180,18 +180,6 @@ export const createCollectionAndInsertData = async ({ hashRawText: rawText ? hashStr(rawText) : undefined, rawTextLength: rawText?.length, - nextSyncTime: (() => { - // ignore auto collections sync for website datasets - if (!dataset.autoSync && dataset.type === DatasetTypeEnum.websiteDataset) return undefined; - if ( - [DatasetCollectionTypeEnum.link, DatasetCollectionTypeEnum.apiFile].includes( - formatCreateCollectionParams.type - ) - ) { - return addDays(new Date(), 1); - } - return undefined; - })(), session }); @@ -285,7 +273,8 @@ export async function createOneCollection({ session, ...props }: CreateOneCollec rawLink, externalFileId, externalFileUrl, - apiFileId + apiFileId, + apiFileParentId } = props; const collectionTags = await createOrGetCollectionTags({ @@ -310,7 +299,8 @@ export async function createOneCollection({ session, ...props }: CreateOneCollec ...(rawLink ? { rawLink } : {}), ...(externalFileId ? { externalFileId } : {}), ...(externalFileUrl ? { externalFileUrl } : {}), - ...(apiFileId ? { apiFileId } : {}) + ...(apiFileId ? { apiFileId } : {}), + ...(apiFileParentId ? { apiFileParentId } : {}) } ], { session, ordered: true } diff --git a/packages/service/core/dataset/collection/schema.ts b/packages/service/core/dataset/collection/schema.ts index 674275520..61ddc48a0 100644 --- a/packages/service/core/dataset/collection/schema.ts +++ b/packages/service/core/dataset/collection/schema.ts @@ -78,11 +78,10 @@ const DatasetCollectionSchema = new Schema({ }, forbid: Boolean, - // next sync time - nextSyncTime: Date, // Parse settings customPdfParse: Boolean, + apiFileParentId: String, // Chunk settings ...ChunkSettings @@ -112,16 +111,6 @@ try { // create time filter DatasetCollectionSchema.index({ teamId: 1, datasetId: 1, createTime: 1 }); - // next sync time filter - DatasetCollectionSchema.index( - { type: 1, nextSyncTime: -1 }, - { - partialFilterExpression: { - nextSyncTime: { $exists: true } - } - } - ); - // Get collection by external file id DatasetCollectionSchema.index( { datasetId: 1, externalFileId: 1 }, diff --git a/packages/service/core/dataset/collection/utils.ts b/packages/service/core/dataset/collection/utils.ts index 4eb964587..e2334ef0b 100644 --- a/packages/service/core/dataset/collection/utils.ts +++ b/packages/service/core/dataset/collection/utils.ts @@ -173,37 +173,39 @@ export const syncCollection = async (collection: CollectionWithDatasetType) => { // Check if the original text is the same: skip if same const hashRawText = hashStr(rawText); - if (collection.hashRawText && hashRawText === collection.hashRawText) { - return DatasetCollectionSyncResultEnum.sameRaw; + if (collection.hashRawText && hashRawText !== collection.hashRawText) { + await mongoSessionRun(async (session) => { + // Delete old collection + await delCollection({ + collections: [collection], + delImg: false, + delFile: false, + session + }); + + // Create new collection + await createCollectionAndInsertData({ + session, + dataset, + rawText: rawText, + createCollectionParams: { + ...collection, + name: title || collection.name, + updateTime: new Date(), + tags: await collectionTagsToTagLabel({ + datasetId: collection.datasetId, + tags: collection.tags + }) + } + }); + }); + + return DatasetCollectionSyncResultEnum.success; + } else if (collection.name !== title) { + await MongoDatasetCollection.updateOne({ _id: collection._id }, { $set: { name: title } }); + return DatasetCollectionSyncResultEnum.success; } - - await mongoSessionRun(async (session) => { - // Delete old collection - await delCollection({ - collections: [collection], - delImg: false, - delFile: false, - session - }); - - // Create new collection - await createCollectionAndInsertData({ - session, - dataset, - rawText: rawText, - createCollectionParams: { - ...collection, - name: title || collection.name, - updateTime: new Date(), - tags: await collectionTagsToTagLabel({ - datasetId: collection.datasetId, - tags: collection.tags - }) - } - }); - }); - - return DatasetCollectionSyncResultEnum.success; + return DatasetCollectionSyncResultEnum.sameRaw; }; /* diff --git a/packages/service/core/dataset/websiteSync/index.ts b/packages/service/core/dataset/datasetSync/index.ts similarity index 66% rename from packages/service/core/dataset/websiteSync/index.ts rename to packages/service/core/dataset/datasetSync/index.ts index 7c54b375b..3ff7d4b1c 100644 --- a/packages/service/core/dataset/websiteSync/index.ts +++ b/packages/service/core/dataset/datasetSync/index.ts @@ -2,11 +2,11 @@ import { type Processor } from 'bullmq'; import { getQueue, getWorker, QueueNames } from '../../../common/bullmq'; import { DatasetStatusEnum } from '@fastgpt/global/core/dataset/constants'; -export type WebsiteSyncJobData = { +export type DatasetSyncJobData = { datasetId: string; }; -export const websiteSyncQueue = getQueue(QueueNames.websiteSync, { +export const datasetSyncQueue = getQueue(QueueNames.datasetSync, { defaultJobOptions: { attempts: 3, // retry 3 times backoff: { @@ -15,8 +15,8 @@ export const websiteSyncQueue = getQueue(QueueNames.websiteS } } }); -export const getWebsiteSyncWorker = (processor: Processor) => { - return getWorker(QueueNames.websiteSync, processor, { +export const getDatasetSyncWorker = (processor: Processor) => { + return getWorker(QueueNames.datasetSync, processor, { removeOnFail: { age: 15 * 24 * 60 * 60, // Keep up to 15 days count: 1000 // Keep up to 1000 jobs @@ -25,21 +25,21 @@ export const getWebsiteSyncWorker = (processor: Processor) = }); }; -export const addWebsiteSyncJob = (data: WebsiteSyncJobData) => { +export const addDatasetSyncJob = (data: DatasetSyncJobData) => { const datasetId = String(data.datasetId); // deduplication: make sure only 1 job - return websiteSyncQueue.add(datasetId, data, { deduplication: { id: datasetId } }); + return datasetSyncQueue.add(datasetId, data, { deduplication: { id: datasetId } }); }; -export const getWebsiteSyncDatasetStatus = async (datasetId: string) => { - const jobId = await websiteSyncQueue.getDeduplicationJobId(datasetId); +export const getDatasetSyncDatasetStatus = async (datasetId: string) => { + const jobId = await datasetSyncQueue.getDeduplicationJobId(datasetId); if (!jobId) { return { status: DatasetStatusEnum.active, errorMsg: undefined }; } - const job = await websiteSyncQueue.getJob(jobId); + const job = await datasetSyncQueue.getJob(jobId); if (!job) { return { status: DatasetStatusEnum.active, @@ -76,10 +76,10 @@ export const getWebsiteSyncDatasetStatus = async (datasetId: string) => { // Scheduler setting const repeatDuration = 24 * 60 * 60 * 1000; // every day -export const upsertWebsiteSyncJobScheduler = (data: WebsiteSyncJobData, startDate?: number) => { +export const upsertDatasetSyncJobScheduler = (data: DatasetSyncJobData, startDate?: number) => { const datasetId = String(data.datasetId); - return websiteSyncQueue.upsertJobScheduler( + return datasetSyncQueue.upsertJobScheduler( datasetId, { every: repeatDuration, @@ -92,10 +92,10 @@ export const upsertWebsiteSyncJobScheduler = (data: WebsiteSyncJobData, startDat ); }; -export const getWebsiteSyncJobScheduler = (datasetId: string) => { - return websiteSyncQueue.getJobScheduler(String(datasetId)); +export const getDatasetSyncJobScheduler = (datasetId: string) => { + return datasetSyncQueue.getJobScheduler(String(datasetId)); }; -export const removeWebsiteSyncJobScheduler = (datasetId: string) => { - return websiteSyncQueue.removeJobScheduler(String(datasetId)); +export const removeDatasetSyncJobScheduler = (datasetId: string) => { + return datasetSyncQueue.removeJobScheduler(String(datasetId)); }; diff --git a/packages/service/support/permission/teamLimit.ts b/packages/service/support/permission/teamLimit.ts index 2546767a6..3c1a78ab2 100644 --- a/packages/service/support/permission/teamLimit.ts +++ b/packages/service/support/permission/teamLimit.ts @@ -119,7 +119,7 @@ export const checkTeamDatasetLimit = async (teamId: string) => { } }; -export const checkTeamWebSyncPermission = async (teamId: string) => { +export const checkTeamDatasetSyncPermission = async (teamId: string) => { const { standardConstants } = await getTeamStandPlan({ teamId }); diff --git a/packages/web/common/file/hooks/useSelectFile.tsx b/packages/web/common/file/hooks/useSelectFile.tsx index 82852681d..5f83dce0a 100644 --- a/packages/web/common/file/hooks/useSelectFile.tsx +++ b/packages/web/common/file/hooks/useSelectFile.tsx @@ -41,7 +41,7 @@ export const useSelectFile = (props?: { /> ), - [fileType, maxCount, multiple, toast] + [fileType, maxCount, multiple, t, toast] ); const onOpen = useCallback((sign?: any) => { diff --git a/packages/web/components/common/MySelect/MultipleSelect.tsx b/packages/web/components/common/MySelect/MultipleSelect.tsx index 0dfd8612d..e57d2c01b 100644 --- a/packages/web/components/common/MySelect/MultipleSelect.tsx +++ b/packages/web/components/common/MySelect/MultipleSelect.tsx @@ -192,7 +192,7 @@ const MultipleSelect = ({ bg={'primary.100'} color={'primary.700'} type={'fill'} - borderRadius={'full'} + borderRadius={'lg'} px={2} py={0.5} flexShrink={0} diff --git a/packages/web/components/common/MySelect/index.tsx b/packages/web/components/common/MySelect/index.tsx index 6a17f2b92..a83010777 100644 --- a/packages/web/components/common/MySelect/index.tsx +++ b/packages/web/components/common/MySelect/index.tsx @@ -54,6 +54,9 @@ export type SelectProps = Omit & { ScrollData?: ReturnType['ScrollData']; customOnOpen?: () => void; customOnClose?: () => void; + + isInvalid?: boolean; + isDisabled?: boolean; }; export const menuItemStyles: MenuItemProps = { @@ -82,6 +85,8 @@ const MySelect = ( ScrollData, customOnOpen, customOnClose, + isInvalid, + isDisabled, ...props }: SelectProps, ref: ForwardedRef<{ @@ -213,16 +218,31 @@ const MySelect = ( h={'auto'} whiteSpace={'pre-wrap'} wordBreak={'break-word'} + transition={'border-color 0.1s ease-in-out, box-shadow 0.1s ease-in-out'} + isDisabled={isDisabled} _active={{ transform: 'none' }} - {...(isOpen - ? { - boxShadow: '0px 0px 0px 2.4px rgba(51, 112, 255, 0.15)', - borderColor: 'primary.600', - color: 'primary.700' - } - : {})} + color={isOpen ? 'primary.700' : 'myGray.700'} + borderColor={isInvalid ? 'red.500' : isOpen ? 'primary.300' : 'myGray.200'} + boxShadow={ + isOpen + ? isInvalid + ? '0px 0px 0px 2.4px rgba(255, 0, 0, 0.15)' + : '0px 0px 0px 2.4px rgba(51, 112, 255, 0.15)' + : 'none' + } + _hover={ + isInvalid + ? { + borderColor: 'red.400', + boxShadow: '0px 0px 0px 2.4px rgba(255, 0, 0, 0.15)' + } + : { + borderColor: 'primary.300', + boxShadow: '0px 0px 0px 2.4px rgba(51, 112, 255, 0.15)' + } + } {...props} > diff --git a/packages/web/components/common/Textarea/JsonEditor/index.tsx b/packages/web/components/common/Textarea/JsonEditor/index.tsx index da26e20ad..2c8adf1e5 100644 --- a/packages/web/components/common/Textarea/JsonEditor/index.tsx +++ b/packages/web/components/common/Textarea/JsonEditor/index.tsx @@ -230,12 +230,24 @@ const JSONEditor = ({ return ( {resize && ( @@ -291,6 +303,19 @@ const JSONEditor = ({ > {placeholder} + {isDisabled && ( + + )} ); }; diff --git a/packages/web/components/common/Textarea/PromptEditor/Editor.tsx b/packages/web/components/common/Textarea/PromptEditor/Editor.tsx index 32a3491d1..4167232bb 100644 --- a/packages/web/components/common/Textarea/PromptEditor/Editor.tsx +++ b/packages/web/components/common/Textarea/PromptEditor/Editor.tsx @@ -21,6 +21,7 @@ import { VariableNode } from './plugins/VariablePlugin/node'; import type { EditorState, LexicalEditor } from 'lexical'; import OnBlurPlugin from './plugins/OnBlurPlugin'; import MyIcon from '../../Icon'; +import type { FormPropsType } from './type.d'; import { type EditorVariableLabelPickerType, type EditorVariablePickerType } from './type.d'; import { getNanoid } from '@fastgpt/global/common/string/tools'; import FocusPlugin from './plugins/FocusPlugin'; @@ -43,7 +44,9 @@ export default function Editor({ onBlur, value, placeholder = '', - bg = 'white' + bg = 'white', + + isInvalid }: { minH?: number; maxH?: number; @@ -56,8 +59,9 @@ export default function Editor({ onBlur?: (editor: LexicalEditor) => void; value?: string; placeholder?: string; - bg?: string; -}) { + + isInvalid?: boolean; +} & FormPropsType) { const [key, setKey] = useState(getNanoid(6)); const [_, startSts] = useTransition(); const [focus, setFocus] = useState(false); @@ -91,7 +95,7 @@ export default function Editor({ { + + isInvalid?: boolean; + isDisabled?: boolean; +} & FormPropsType) => { const { isOpen, onOpen, onClose } = useDisclosure(); const { t } = useTranslation(); @@ -55,20 +61,36 @@ const PromptEditor = ({ return ( <> - + + + {isDisabled && ( + + )} + ; diff --git a/packages/web/hooks/useRefresh.ts b/packages/web/hooks/useRefresh.ts index a72e45774..da421a029 100644 --- a/packages/web/hooks/useRefresh.ts +++ b/packages/web/hooks/useRefresh.ts @@ -1,9 +1,10 @@ import { useBoolean } from 'ahooks'; export const useRefresh = () => { - const [_, { toggle }] = useBoolean(); + const [refreshSign, { toggle }] = useBoolean(); return { - refresh: toggle + refresh: toggle, + refreshSign }; }; diff --git a/packages/web/hooks/useSafeTranslation.ts b/packages/web/hooks/useSafeTranslation.ts new file mode 100644 index 000000000..d52050f64 --- /dev/null +++ b/packages/web/hooks/useSafeTranslation.ts @@ -0,0 +1,23 @@ +import { useTranslation as useNextTranslation } from 'next-i18next'; +import type { I18nNsType } from '../i18n/i18next'; +import { I18N_NAMESPACES_MAP } from '../i18n/constants'; + +export function useTranslation(ns?: I18nNsType[0] | I18nNsType) { + const { t: originalT, ...rest } = useNextTranslation(ns); + + const t = (key: string | undefined, ...args: any[]): string => { + if (!key) return ''; + + if (!I18N_NAMESPACES_MAP[key as any]) { + return key; + } + + // @ts-ignore + return originalT(key, ...args); + }; + + return { + t, + ...rest + }; +} diff --git a/packages/web/i18n/constants.ts b/packages/web/i18n/constants.ts new file mode 100644 index 000000000..dd0a2a5e8 --- /dev/null +++ b/packages/web/i18n/constants.ts @@ -0,0 +1,31 @@ +export const I18N_NAMESPACES = [ + 'common', + 'dataset', + 'app', + 'file', + 'publish', + 'workflow', + 'user', + 'chat', + 'login', + 'account_info', + 'account_usage', + 'account_bill', + 'account_apikey', + 'account_setting', + 'account_inform', + 'account_promotion', + 'account_thirdParty', + 'account', + 'account_team', + 'account_model', + 'dashboard_mcp' +]; + +export const I18N_NAMESPACES_MAP = I18N_NAMESPACES.reduce( + (acc, namespace) => { + acc[namespace] = true; + return acc; + }, + {} as Record +); diff --git a/packages/web/i18n/en/chat.json b/packages/web/i18n/en/chat.json index 211800445..1f6a67e92 100644 --- a/packages/web/i18n/en/chat.json +++ b/packages/web/i18n/en/chat.json @@ -77,6 +77,7 @@ "select_file_img": "Upload file / image", "select_img": "Upload Image", "source_cronJob": "Scheduled execution", + "start_chat": "Start", "stream_output": "Stream Output", "unsupported_file_type": "Unsupported file types", "upload": "Upload", diff --git a/packages/web/i18n/en/common.json b/packages/web/i18n/en/common.json index 894ece4ed..38cc970b7 100644 --- a/packages/web/i18n/en/common.json +++ b/packages/web/i18n/en/common.json @@ -77,7 +77,6 @@ "Save": "Save", "Save_and_exit": "Save and Exit", "Search": "Search", - "Select_all": "Select all", "Setting": "Setting", "Status": "Status", "Submit": "Submit", @@ -355,7 +354,6 @@ "core.chat.Select dataset Desc": "Select a Dataset to store the expected answer", "core.chat.Send Message": "Send", "core.chat.Speaking": "I'm Listening, Please Speak...", - "core.chat.Start Chat": "Start Chat", "core.chat.Type a message": "Enter a Question, Press [Enter] to Send / Press [Ctrl(Alt/Shift) + Enter] for New Line", "core.chat.Unpin": "Unpin", "core.chat.You need to a chat app": "You Do Not Have an Available App", diff --git a/packages/web/i18n/en/dataset.json b/packages/web/i18n/en/dataset.json index 009a4f748..d8884ad42 100644 --- a/packages/web/i18n/en/dataset.json +++ b/packages/web/i18n/en/dataset.json @@ -1,5 +1,6 @@ { "Enable": "Enable", + "Select_all": "Select all files", "add_file": "Import", "api_file": "API Dataset", "api_url": "API Url", @@ -24,6 +25,7 @@ "close_auto_sync": "Are you sure you want to turn off automatic sync?", "collection.Create update time": "Creation/Update Time", "collection.Training type": "Training", + "collection.sync.submit": "The synchronization task has been submitted", "collection.training_type": "Chunk type", "collection_data_count": "Data amount", "collection_metadata_custom_pdf_parse": "PDF enhancement analysis", @@ -147,6 +149,7 @@ "pleaseFillUserIdAndToken": "Please fill in User ID and Token", "preview_chunk": "Preview chunks", "preview_chunk_empty": "File content is empty", + "preview_chunk_folder_warning": "Directory does not support preview", "preview_chunk_intro": "A total of {{total}} blocks, up to 10", "preview_chunk_not_selected": "Click on the file on the left to preview", "process.Auto_Index": "Automatic index generation", @@ -178,7 +181,7 @@ "split_sign_period": "period", "split_sign_question": "question mark", "split_sign_semicolon": "semicolon", - "start_sync_website_tip": "Confirm to start synchronizing data? \nThe old data will be deleted and retrieved again, please confirm!", + "start_sync_dataset_tip": "Do you really start synchronizing the entire knowledge base?", "status_error": "Running exception", "sync_collection_failed": "Synchronization collection error, please check whether the source file can be accessed normally", "sync_schedule": "Timing synchronization", diff --git a/packages/web/i18n/i18next.d.ts b/packages/web/i18n/i18next.d.ts new file mode 100644 index 000000000..a22cff1e0 --- /dev/null +++ b/packages/web/i18n/i18next.d.ts @@ -0,0 +1,65 @@ +import 'i18next'; +import type account_team from './zh-CN/account_team.json'; +import type account from './zh-CN/account.json'; +import type account_thirdParty from './zh-CN/account_thirdParty.json'; +import type account_promotion from './zh-CN/account_promotion.json'; +import type account_inform from './zh-CN/account_inform.json'; +import type account_setting from './zh-CN/account_setting.json'; +import type account_apikey from './zh-CN/account_apikey.json'; +import type account_bill from './zh-CN/account_bill.json'; +import type account_usage from './zh-CN/account_usage.json'; +import type account_info from './zh-CN/account_info.json'; +import type common from './zh-CN/common.json'; +import type dataset from './zh-CN/dataset.json'; +import type app from './zh-CN/app.json'; +import type file from './zh-CN/file.json'; +import type publish from './zh-CN/publish.json'; +import type workflow from './zh-CN/workflow.json'; +import type user from './zh-CN/user.json'; +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 { I18N_NAMESPACES } from './constants'; + +export interface I18nNamespaces { + common: typeof common; + dataset: typeof dataset; + app: typeof app; + file: typeof file; + publish: typeof publish; + workflow: typeof workflow; + user: typeof user; + chat: typeof chat; + login: typeof login; + account_info: typeof account_info; + account_usage: typeof account_usage; + account_bill: typeof account_bill; + account_apikey: typeof account_apikey; + account_setting: typeof account_setting; + account_inform: typeof account_inform; + account_promotion: typeof account_promotion; + account: typeof account; + account_team: typeof account_team; + account_thirdParty: typeof account_thirdParty; + account_model: typeof account_model; + dashboard_mcp: typeof dashboard_mcp; +} + +export type I18nNsType = (keyof I18nNamespaces)[]; + +export type ParseKeys = { + [K in Ns]: `${K}:${keyof I18nNamespaces[K] & string}`; +}[Ns]; + +export type I18nKeyFunction = { + (key: Key): Key; +}; + +declare module 'i18next' { + interface CustomTypeOptions { + returnNull: false; + defaultNS: I18N_NAMESPACES; + resources: I18nNamespaces; + } +} diff --git a/packages/web/i18n/utils.ts b/packages/web/i18n/utils.ts index 684d19854..9b2aae584 100644 --- a/packages/web/i18n/utils.ts +++ b/packages/web/i18n/utils.ts @@ -1,3 +1,3 @@ -import { type I18nKeyFunction } from '../types/i18next'; +import { type I18nKeyFunction } from './i18next'; export const i18nT: I18nKeyFunction = (key) => key; diff --git a/packages/web/i18n/zh-CN/account_team.json b/packages/web/i18n/zh-CN/account_team.json index 81b3f7da0..8eb175c41 100644 --- a/packages/web/i18n/zh-CN/account_team.json +++ b/packages/web/i18n/zh-CN/account_team.json @@ -109,7 +109,7 @@ "join_update_time": "加入/更新时间", "kick_out_team": "移除成员", "label_sync": "标签同步", - "leave": "已离职", + "leave": "离开", "leave_team_failed": "离开团队异常", "log_admin_add_plan": "【{{name}}】将给团队id为【{{teamId}}】的团队添加了套餐", "log_admin_add_user": "【{{name}}】创建了一个名为【{{userName}}】的用户", @@ -218,7 +218,7 @@ "recover_team_member": "成员恢复", "relocate_department": "部门移动", "remark": "备注", - "remove_tip": "确认将 {{username}} 移出团队?成员将被标记为“已离职”,不删除操作数据,账号下资源自动转让给团队所有者。", + "remove_tip": "确认将 {{username}} 移出团队?成员将被标记为“离开”,不删除操作数据,账号下资源自动转让给团队所有者。", "restore_tip": "确认将 {{username}} 加入团队吗?仅恢复该成员账号可用性及相关权限,无法恢复账号下资源。", "restore_tip_title": "恢复确认", "retain_admin_permissions": "保留管理员权限", diff --git a/packages/web/i18n/zh-CN/chat.json b/packages/web/i18n/zh-CN/chat.json index 87307af07..89fad715a 100644 --- a/packages/web/i18n/zh-CN/chat.json +++ b/packages/web/i18n/zh-CN/chat.json @@ -77,6 +77,7 @@ "select_file_img": "上传文件/图片", "select_img": "上传图片", "source_cronJob": "定时执行", + "start_chat": "开始对话", "stream_output": "流输出", "unsupported_file_type": "不支持的文件类型", "upload": "上传", diff --git a/packages/web/i18n/zh-CN/common.json b/packages/web/i18n/zh-CN/common.json index 2b474ce3f..83e3e4694 100644 --- a/packages/web/i18n/zh-CN/common.json +++ b/packages/web/i18n/zh-CN/common.json @@ -77,7 +77,6 @@ "Save": "保存", "Save_and_exit": "保存并退出", "Search": "搜索", - "Select_all": "全选", "Setting": "设置", "Status": "状态", "Submit": "提交", @@ -355,7 +354,6 @@ "core.chat.Select dataset Desc": "选择一个知识库存储预期答案", "core.chat.Send Message": "发送", "core.chat.Speaking": "我在听,请说...", - "core.chat.Start Chat": "开始对话", "core.chat.Type a message": "输入问题,发送 [Enter]/换行 [Ctrl(Alt/Shift) + Enter]", "core.chat.Unpin": "取消置顶", "core.chat.You need to a chat app": "你没有可用的应用", @@ -1301,7 +1299,7 @@ "user.team.role.Visitor": "访客", "user.team.role.writer": "可写成员", "user.type": "类型", - "user_leaved": "已离开", + "user_leaved": "离开", "value": "值", "verification": "验证", "xx_search_result": "{{key}} 的搜索结果", diff --git a/packages/web/i18n/zh-CN/dataset.json b/packages/web/i18n/zh-CN/dataset.json index 5f6fadb49..539961490 100644 --- a/packages/web/i18n/zh-CN/dataset.json +++ b/packages/web/i18n/zh-CN/dataset.json @@ -1,5 +1,6 @@ { "Enable": "启用", + "Select_all": "选择所有文件", "add_file": "添加文件", "api_file": "API 文件库", "api_url": "接口地址", @@ -24,6 +25,7 @@ "close_auto_sync": "确认关闭自动同步功能?", "collection.Create update time": "创建/更新时间", "collection.Training type": "训练模式", + "collection.sync.submit": "已提交同步任务", "collection.training_type": "处理模式", "collection_data_count": "数据量", "collection_metadata_custom_pdf_parse": "PDF增强解析", @@ -147,6 +149,7 @@ "pleaseFillUserIdAndToken": "请填写 User ID 和 Token", "preview_chunk": "分块预览", "preview_chunk_empty": "文件内容为空", + "preview_chunk_folder_warning": "目录不支持预览", "preview_chunk_intro": "共 {{total}} 个分块,最多展示 10 个", "preview_chunk_not_selected": "点击左侧文件后进行预览", "process.Auto_Index": "自动索引生成", @@ -178,7 +181,7 @@ "split_sign_period": "句号", "split_sign_question": "问号", "split_sign_semicolon": "分号", - "start_sync_website_tip": "确认开始同步数据?将会删除旧数据后重新获取,请确认!", + "start_sync_dataset_tip": "确实开始同步整个知识库?", "status_error": "运行异常", "sync_collection_failed": "同步集合错误,请检查是否能正常访问源文件", "sync_schedule": "定时同步", diff --git a/packages/web/i18n/zh-Hant/chat.json b/packages/web/i18n/zh-Hant/chat.json index 34ff1f7b3..1a1d24ea6 100644 --- a/packages/web/i18n/zh-Hant/chat.json +++ b/packages/web/i18n/zh-Hant/chat.json @@ -77,6 +77,7 @@ "select_file_img": "上傳檔案 / 圖片", "select_img": "上傳圖片", "source_cronJob": "定時執行", + "start_chat": "開始對話", "stream_output": "串流輸出", "unsupported_file_type": "不支援的檔案類型", "upload": "上傳", diff --git a/packages/web/i18n/zh-Hant/common.json b/packages/web/i18n/zh-Hant/common.json index 8ecfbcef5..d3c79d49e 100644 --- a/packages/web/i18n/zh-Hant/common.json +++ b/packages/web/i18n/zh-Hant/common.json @@ -77,7 +77,6 @@ "Save": "儲存", "Save_and_exit": "儲存並離開", "Search": "搜尋", - "Select_all": "全選", "Setting": "設定", "Status": "狀態", "Submit": "送出", @@ -355,7 +354,6 @@ "core.chat.Select dataset Desc": "選擇一個知識庫來儲存預期回答", "core.chat.Send Message": "傳送", "core.chat.Speaking": "我在聽,請說...", - "core.chat.Start Chat": "開始對話", "core.chat.Type a message": "輸入問題,按 [Enter] 傳送 / 按 [Ctrl(Alt/Shift) + Enter] 換行", "core.chat.Unpin": "取消釘選", "core.chat.You need to a chat app": "您沒有可用的應用程式", diff --git a/packages/web/i18n/zh-Hant/dataset.json b/packages/web/i18n/zh-Hant/dataset.json index 504c1d86d..e86d9f8e7 100644 --- a/packages/web/i18n/zh-Hant/dataset.json +++ b/packages/web/i18n/zh-Hant/dataset.json @@ -1,5 +1,6 @@ { "Enable": "啟用", + "Select_all": "選中所有檔案", "add_file": "新增文件", "api_file": "API 檔案庫", "api_url": "介面位址", @@ -24,6 +25,7 @@ "close_auto_sync": "確認關閉自動同步功能?", "collection.Create update time": "建立/更新時間", "collection.Training type": "分段模式", + "collection.sync.submit": "已提交同步任務", "collection.training_type": "處理模式", "collection_data_count": "資料量", "collection_metadata_custom_pdf_parse": "PDF 增強解析", @@ -147,6 +149,7 @@ "pleaseFillUserIdAndToken": "請填寫 User ID 和 Token", "preview_chunk": "分塊預覽", "preview_chunk_empty": "文件內容為空", + "preview_chunk_folder_warning": "目錄不支持預覽", "preview_chunk_intro": "共 {{total}} 個分塊,最多展示 10 個", "preview_chunk_not_selected": "點選左側文件後進行預覽", "process.Auto_Index": "自動索引生成", @@ -178,7 +181,7 @@ "split_sign_period": "句號", "split_sign_question": "問號", "split_sign_semicolon": "分號", - "start_sync_website_tip": "確認開始同步資料?\n將會刪除舊資料後重新取得,請確認!", + "start_sync_dataset_tip": "確實開始同步整個知識庫?", "status_error": "執行異常", "sync_collection_failed": "同步集合錯誤,請檢查是否能正常存取來原始檔", "sync_schedule": "定時同步", diff --git a/packages/web/styles/theme.ts b/packages/web/styles/theme.ts index 53b96f12c..737ebc08a 100644 --- a/packages/web/styles/theme.ts +++ b/packages/web/styles/theme.ts @@ -382,14 +382,31 @@ const NumberInput = numInputMultiStyle({ bg: 'myGray.50', border: '1px solid', borderColor: 'myGray.200', + borderRadius: 'sm', + transition: 'border-color 0.1s ease-in-out, box-shadow 0.1s ease-in-out', + _hover: { + borderColor: 'primary.300' + }, _focus: { - borderColor: 'primary.500 !important', + borderColor: 'primary.600 !important', boxShadow: `${shadowLight} !important`, bg: 'white' }, _disabled: { color: 'myGray.400 !important', bg: 'myWhite.300 !important' + }, + _invalid: { + borderColor: 'red.500 !important', + borderWidth: '1px !important', + boxShadow: 'none !important', + _hover: { + borderColor: 'red.400 !important' + }, + _focus: { + borderColor: 'red.600 !important', + boxShadow: '0px 0px 0px 2.4px rgba(244, 69, 46, 0.15) !important' + } } }, stepper: { diff --git a/packages/web/types/i18next.d.ts b/packages/web/types/i18next.d.ts deleted file mode 100644 index 5a2903e72..000000000 --- a/packages/web/types/i18next.d.ts +++ /dev/null @@ -1,86 +0,0 @@ -import 'i18next'; -import type account_team from '../i18n/zh-CN/account_team.json'; -import type account from '../i18n/zh-CN/account.json'; -import type account_thirdParty from '../i18n/zh-CN/account_thirdParty.json'; -import type account_promotion from '../i18n/zh-CN/account_promotion.json'; -import type account_inform from '../i18n/zh-CN/account_inform.json'; -import type account_setting from '../i18n/zh-CN/account_setting.json'; -import type account_apikey from '../i18n/zh-CN/account_apikey.json'; -import type account_bill from '../i18n/zh-CN/account_bill.json'; -import type account_usage from '../i18n/zh-CN/account_usage.json'; -import type account_info from '../i18n/zh-CN/account_info.json'; -import type common from '../i18n/zh-CN/common.json'; -import type dataset from '../i18n/zh-CN/dataset.json'; -import type app from '../i18n/zh-CN/app.json'; -import type file from '../i18n/zh-CN/file.json'; -import type publish from '../i18n/zh-CN/publish.json'; -import type workflow from '../i18n/zh-CN/workflow.json'; -import type user from '../i18n/zh-CN/user.json'; -import type chat from '../i18n/zh-CN/chat.json'; -import type login from '../i18n/zh-CN/login.json'; -import type account_model from '../i18n/zh-CN/account_model.json'; -import type dashboard_mcp from '../i18n/zh-CN/dashboard_mcp.json'; - -export interface I18nNamespaces { - common: typeof common; - dataset: typeof dataset; - app: typeof app; - file: typeof file; - publish: typeof publish; - workflow: typeof workflow; - user: typeof user; - chat: typeof chat; - login: typeof login; - account_info: typeof account_info; - account_usage: typeof account_usage; - account_bill: typeof account_bill; - account_apikey: typeof account_apikey; - account_setting: typeof account_setting; - account_inform: typeof account_inform; - account_promotion: typeof account_promotion; - account: typeof account; - account_team: typeof account_team; - account_thirdParty: typeof account_thirdParty; - account_model: typeof account_model; - dashboard_mcp: typeof dashboard_mcp; -} - -export type I18nNsType = (keyof I18nNamespaces)[]; - -export type ParseKeys = { - [K in Ns]: `${K}:${keyof I18nNamespaces[K] & string}`; -}[Ns]; - -export type I18nKeyFunction = { - (key: Key): Key; -}; - -declare module 'i18next' { - interface CustomTypeOptions { - returnNull: false; - defaultNS: [ - 'common', - 'dataset', - 'app', - 'file', - 'publish', - 'workflow', - 'user', - 'chat', - 'login', - 'account_info', - 'account_usage', - 'account_bill', - 'account_apikey', - 'account_setting', - 'account_inform', - 'account_promotion', - 'account_thirdParty', - 'account', - 'account_team', - 'account_model', - 'dashboard_mcp' - ]; - resources: I18nNamespaces; - } -} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b1df91099..3343f3281 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -20520,4 +20520,4 @@ snapshots: immer: 9.0.21 react: 18.3.1 - zwitch@2.0.4: {} + zwitch@2.0.4: {} \ No newline at end of file diff --git a/projects/app/src/components/Select/FileSelector.tsx b/projects/app/src/components/Select/FileSelector.tsx new file mode 100644 index 000000000..9fc335d00 --- /dev/null +++ b/projects/app/src/components/Select/FileSelector.tsx @@ -0,0 +1,125 @@ +import { useTranslation } from 'react-i18next'; +import type { UseFormReturn } from 'react-hook-form'; +import { useFieldArray } from 'react-hook-form'; +import { useFileUpload } from '../core/chat/ChatContainer/ChatBox/hooks/useFileUpload'; +import { useEffect } from 'react'; +import { isEqual } from 'lodash'; +import { useRequest2 } from '@fastgpt/web/hooks/useRequest'; +import { Button, Flex } from '@chakra-ui/react'; +import MyIcon from '@fastgpt/web/components/common/Icon'; +import FilePreview from '../core/chat/ChatContainer/components/FilePreview'; +import EmptyTip from '@fastgpt/web/components/common/EmptyTip'; +import { useChatStore } from '@/web/core/chat/context/useChatStore'; + +const FileSelector = ({ + onChange, + value, + + form, + fieldName, + + canSelectFile = true, + canSelectImg = false, + maxFiles = 5, + setUploading, + + isDisabled = false +}: { + onChange: (...event: any[]) => void; + value: any; + + form?: UseFormReturn; + fieldName?: string; + + canSelectFile?: boolean; + canSelectImg?: boolean; + maxFiles?: number; + setUploading?: (uploading: boolean) => void; + + isDisabled?: boolean; +}) => { + const { t } = useTranslation(); + + const { appId, chatId, outLinkAuthData } = useChatStore(); + const fileCtrl = useFieldArray({ + control: form?.control, + name: fieldName as any + }); + const { + File, + fileList, + selectFileIcon, + uploadFiles, + onOpenSelectFile, + onSelectFile, + removeFiles, + replaceFiles, + hasFileUploading + } = useFileUpload({ + fileSelectConfig: { + canSelectFile, + canSelectImg, + maxFiles + }, + outLinkAuthData, + appId, + chatId, + fileCtrl: fileCtrl as any + }); + + useEffect(() => { + if (!Array.isArray(value)) { + replaceFiles([]); + return; + } + + // compare file names and update if different + const valueFileNames = value.map((item) => item.name); + const currentFileNames = fileList.map((item) => item.name); + if (!isEqual(valueFileNames, currentFileNames)) { + replaceFiles(value); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [value]); + + useRequest2(uploadFiles, { + manual: false, + errorToast: t('common:upload_file_error'), + refreshDeps: [fileList] + }); + + useEffect(() => { + setUploading?.(hasFileUploading); + onChange( + fileList.map((item) => ({ + type: item.type, + name: item.name, + url: item.url, + icon: item.icon + })) + ); + }, [fileList, hasFileUploading, onChange, setUploading]); + + return ( + <> + + + + + {fileList.length === 0 && } + + onSelectFile({ files })} /> + + ); +}; + +export default FileSelector; diff --git a/projects/app/src/components/common/folder/Path.tsx b/projects/app/src/components/common/folder/Path.tsx index 03417e26d..e6c04dd0d 100644 --- a/projects/app/src/components/common/folder/Path.tsx +++ b/projects/app/src/components/common/folder/Path.tsx @@ -116,7 +116,7 @@ const FolderPath = (props: { py={0.5} px={1.5} borderRadius={'sm'} - maxW={'45vw'} + maxW={['45vw', '250px']} className={'textEllipsis'} {...(isLast && concatPaths.length > 1 ? { diff --git a/projects/app/src/components/core/app/formRender/LabelAndForm.tsx b/projects/app/src/components/core/app/formRender/LabelAndForm.tsx new file mode 100644 index 000000000..ea0c35e32 --- /dev/null +++ b/projects/app/src/components/core/app/formRender/LabelAndForm.tsx @@ -0,0 +1,79 @@ +import type { BoxProps } from '@chakra-ui/react'; +import { Box, Flex } from '@chakra-ui/react'; +import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel'; +import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip'; +import React, { useMemo } from 'react'; +import type { UseFormReturn } from 'react-hook-form'; +import { Controller } from 'react-hook-form'; +import InputRender from '.'; +import type { SpecificProps } from './type'; + +// Helper function to flatten error object keys +const getFlattenedErrorKeys = (errors: any, prefix = ''): string[] => { + const keys: string[] = []; + + if (!errors || typeof errors !== 'object') return keys; + + Object.keys(errors).forEach((key) => { + const fullKey = prefix ? `${prefix}.${key}` : key; + keys.push(fullKey); + + // If the error value is an object (nested errors), recursively flatten + if (errors[key] && typeof errors[key] === 'object' && !errors[key].message) { + keys.push(...getFlattenedErrorKeys(errors[key], fullKey)); + } + }); + + return keys; +}; + +const LabelAndFormRender = ({ + formKey, + label, + required, + placeholder, + inputType, + variablesForm, + ...props +}: { + formKey: string; + label: string; + required?: boolean; + placeholder?: string; + variablesForm: UseFormReturn; +} & SpecificProps & + BoxProps) => { + const { control } = variablesForm; + + return ( + + + {label} + {placeholder && } + + + { + return ( + + ); + }} + /> + + ); +}; + +export default LabelAndFormRender; diff --git a/projects/app/src/components/core/app/formRender/constant.ts b/projects/app/src/components/core/app/formRender/constant.ts new file mode 100644 index 000000000..70f4ba299 --- /dev/null +++ b/projects/app/src/components/core/app/formRender/constant.ts @@ -0,0 +1,14 @@ +export enum InputTypeEnum { + input = 'input', + textarea = 'textarea', + numberInput = 'numberInput', + switch = 'switch', + select = 'select', + multipleSelect = 'multipleSelect', + + JSONEditor = 'JSONEditor', + + selectLLMModel = 'selectLLMModel', + + fileSelect = 'fileSelect' +} diff --git a/projects/app/src/components/core/app/formRender/index.tsx b/projects/app/src/components/core/app/formRender/index.tsx new file mode 100644 index 000000000..a4b04e718 --- /dev/null +++ b/projects/app/src/components/core/app/formRender/index.tsx @@ -0,0 +1,165 @@ +import React from 'react'; +import { Box, Switch } from '@chakra-ui/react'; +import type { InputRenderProps } from './type'; +import { InputTypeEnum } from './constant'; +import PromptEditor from '@fastgpt/web/components/common/Textarea/PromptEditor'; +import MyNumberInput from '@fastgpt/web/components/common/Input/NumberInput'; +import MySelect from '@fastgpt/web/components/common/MySelect'; +import MultipleSelect, { + useMultipleSelect +} from '@fastgpt/web/components/common/MySelect/MultipleSelect'; +import JSONEditor from '@fastgpt/web/components/common/Textarea/JsonEditor'; +import AIModelSelector from '../../../Select/AIModelSelector'; +import FileSelector from '../../../Select/FileSelector'; +import { useTranslation } from '@fastgpt/web/hooks/useSafeTranslation'; + +const InputRender = (props: InputRenderProps) => { + const { + inputType, + customRender, + value, + onChange, + isDisabled, + isInvalid, + placeholder, + bg = 'white' + } = props; + if (customRender) { + return <>{customRender(props)}; + } + + const { t } = useTranslation(); + const { + value: selectedValue, + setValue, + isSelectAll, + setIsSelectAll + } = useMultipleSelect( + value, + inputType === InputTypeEnum.multipleSelect && value.length === (props.list?.length || 0) + ); + + const commonProps = { + value, + onChange, + isDisabled, + isInvalid, + placeholder: t(placeholder as any), + bg + }; + + const renderInput = () => { + if (inputType === InputTypeEnum.input) { + return ( + + ); + } + + if (inputType === InputTypeEnum.textarea) { + return ( + + ); + } + + if (inputType === InputTypeEnum.numberInput) { + return ( + + ); + } + + if (inputType === InputTypeEnum.switch) { + return ( + onChange(e.target.checked)} + isDisabled={isDisabled} + /> + ); + } + + if (inputType === InputTypeEnum.select) { + return ; + } + + if (inputType === InputTypeEnum.multipleSelect) { + const { list = [] } = props; + return ( + + {...commonProps} + h={10} + list={list} + value={selectedValue} + onSelect={(val) => { + setValue(val); + onChange(val); + }} + isSelectAll={isSelectAll} + setIsSelectAll={(all) => { + setIsSelectAll(all); + onChange(all ? list.map((item) => item.value) : []); + }} + /> + ); + } + + if (inputType === InputTypeEnum.JSONEditor) { + return ; + } + + if (inputType === InputTypeEnum.selectLLMModel) { + return ( + ({ + value: item.model, + label: item.name + })) || [] + } + /> + ); + } + + if (inputType === InputTypeEnum.fileSelect) { + return ( + + ); + } + + return null; + }; + + return {renderInput()}; +}; + +export default InputRender; diff --git a/projects/app/src/components/core/app/formRender/type.d.ts b/projects/app/src/components/core/app/formRender/type.d.ts new file mode 100644 index 000000000..a31fcab5b --- /dev/null +++ b/projects/app/src/components/core/app/formRender/type.d.ts @@ -0,0 +1,65 @@ +import type { + EditorVariableLabelPickerType, + EditorVariablePickerType +} from '@fastgpt/web/components/common/Textarea/PromptEditor/type'; +import type { InputTypeEnum } from './constant'; +import type { UseFormReturn } from 'react-hook-form'; +import type { BoxProps } from '@chakra-ui/react'; + +type CommonRenderProps = { + placeholder?: string; + value: any; + onChange: (value: any) => void; + + isDisabled?: boolean; + isInvalid?: boolean; + + customRender?: (props: any) => React.ReactNode; +} & Omit; + +type SpecificProps = + | { + // input & textarea + inputType: InputTypeEnum.input | InputTypeEnum.textarea; + variables?: EditorVariablePickerType[]; + variableLabels?: EditorVariableLabelPickerType[]; + title?: string; + maxLength?: number; + } + | { + // numberInput + inputType: InputTypeEnum.numberInput; + min?: number; + max?: number; + } + | { + // switch + inputType: InputTypeEnum.switch; + } + | { + // select & multipleSelect + inputType: InputTypeEnum.select | InputTypeEnum.multipleSelect; + list?: { label: string; value: string }[]; + } + | { + // JSONEditor + inputType: InputTypeEnum.JSONEditor; + } + | { + // selectLLMModel + inputType: InputTypeEnum.selectLLMModel; + modelList?: { model: string; name: string }[]; + } + | { + // fileSelect + inputType: InputTypeEnum.fileSelect; + canSelectFile?: boolean; + canSelectImg?: boolean; + maxFiles?: number; + setUploading?: React.Dispatch>; + + form?: UseFormReturn; + fieldName?: string; + }; + +export type InputRenderProps = CommonRenderProps & SpecificProps; diff --git a/projects/app/src/components/core/app/formRender/utils.ts b/projects/app/src/components/core/app/formRender/utils.ts new file mode 100644 index 000000000..a4da109a6 --- /dev/null +++ b/projects/app/src/components/core/app/formRender/utils.ts @@ -0,0 +1,59 @@ +import { + VariableInputEnum, + WorkflowIOValueTypeEnum +} from '@fastgpt/global/core/workflow/constants'; +import { InputTypeEnum } from './constant'; +import { FlowNodeInputTypeEnum } from '@fastgpt/global/core/workflow/node/constant'; +import type { InputConfigType } from '@fastgpt/global/core/workflow/type/io'; + +export const variableInputTypeToInputType = (inputType: VariableInputEnum) => { + if (inputType === VariableInputEnum.input) return InputTypeEnum.input; + if (inputType === VariableInputEnum.textarea) return InputTypeEnum.textarea; + if (inputType === VariableInputEnum.numberInput) return InputTypeEnum.numberInput; + if (inputType === VariableInputEnum.select) return InputTypeEnum.select; + return InputTypeEnum.textarea; +}; + +// 节点输入类型(通常是一个 reference+一个 form input) +export const nodeInputTypeToInputType = (inputTypes: FlowNodeInputTypeEnum[] = []) => { + const inputType = inputTypes?.find((item) => item !== FlowNodeInputTypeEnum.reference); + + if (inputType === FlowNodeInputTypeEnum.input) return InputTypeEnum.input; + if (inputType === FlowNodeInputTypeEnum.textarea) return InputTypeEnum.textarea; + if (inputType === FlowNodeInputTypeEnum.numberInput) return InputTypeEnum.numberInput; + if (inputType === FlowNodeInputTypeEnum.switch) return InputTypeEnum.switch; + if (inputType === FlowNodeInputTypeEnum.select) return InputTypeEnum.select; + if (inputType === FlowNodeInputTypeEnum.multipleSelect) return InputTypeEnum.multipleSelect; + if (inputType === FlowNodeInputTypeEnum.JSONEditor) return InputTypeEnum.JSONEditor; + if (inputType === FlowNodeInputTypeEnum.selectLLMModel) return InputTypeEnum.selectLLMModel; + if (inputType === FlowNodeInputTypeEnum.fileSelect) return InputTypeEnum.fileSelect; + return InputTypeEnum.textarea; +}; + +export const valueTypeToInputType = (valueType?: WorkflowIOValueTypeEnum) => { + if (valueType === WorkflowIOValueTypeEnum.string) return InputTypeEnum.input; + if (valueType === WorkflowIOValueTypeEnum.number) return InputTypeEnum.numberInput; + if (valueType === WorkflowIOValueTypeEnum.boolean) return InputTypeEnum.switch; + if (valueType === WorkflowIOValueTypeEnum.object) return InputTypeEnum.JSONEditor; + if (valueType === WorkflowIOValueTypeEnum.arrayString) return InputTypeEnum.JSONEditor; + if (valueType === WorkflowIOValueTypeEnum.arrayNumber) return InputTypeEnum.JSONEditor; + if (valueType === WorkflowIOValueTypeEnum.arrayBoolean) return InputTypeEnum.JSONEditor; + if (valueType === WorkflowIOValueTypeEnum.arrayObject) return InputTypeEnum.JSONEditor; + if (valueType === WorkflowIOValueTypeEnum.arrayAny) return InputTypeEnum.JSONEditor; + if (valueType === WorkflowIOValueTypeEnum.chatHistory) return InputTypeEnum.JSONEditor; + if (valueType === WorkflowIOValueTypeEnum.datasetQuote) return InputTypeEnum.JSONEditor; + if (valueType === WorkflowIOValueTypeEnum.dynamic) return InputTypeEnum.JSONEditor; + if (valueType === WorkflowIOValueTypeEnum.selectDataset) return InputTypeEnum.JSONEditor; + if (valueType === WorkflowIOValueTypeEnum.selectApp) return InputTypeEnum.JSONEditor; + if (valueType === WorkflowIOValueTypeEnum.any) return InputTypeEnum.textarea; + + return InputTypeEnum.textarea; +}; + +export const secretInputTypeToInputType = (inputType: InputConfigType['inputType']) => { + if (inputType === 'input') return InputTypeEnum.input; + if (inputType === 'numberInput') return InputTypeEnum.numberInput; + if (inputType === 'switch') return InputTypeEnum.switch; + if (inputType === 'select') return InputTypeEnum.select; + return InputTypeEnum.textarea; +}; diff --git a/projects/app/src/components/core/chat/ChatContainer/ChatBox/components/VariableInput.tsx b/projects/app/src/components/core/chat/ChatContainer/ChatBox/components/VariableInput.tsx deleted file mode 100644 index 1c15c0056..000000000 --- a/projects/app/src/components/core/chat/ChatContainer/ChatBox/components/VariableInput.tsx +++ /dev/null @@ -1,302 +0,0 @@ -import React, { useEffect, useMemo } from 'react'; -import { Controller, type UseFormReturn } from 'react-hook-form'; -import { useTranslation } from 'next-i18next'; -import { Box, Button, Card, Flex, Switch, Textarea } from '@chakra-ui/react'; -import ChatAvatar from './ChatAvatar'; -import { MessageCardStyle } from '../constants'; -import { - VariableInputEnum, - WorkflowIOValueTypeEnum -} from '@fastgpt/global/core/workflow/constants'; -import MySelect from '@fastgpt/web/components/common/MySelect'; -import MyIcon from '@fastgpt/web/components/common/Icon'; -import { type ChatBoxInputFormType } from '../type.d'; -import { useContextSelector } from 'use-context-selector'; -import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip'; -import { type VariableItemType } from '@fastgpt/global/core/app/type'; -import MyTextarea from '@/components/common/Textarea/MyTextarea'; -import MyNumberInput from '@fastgpt/web/components/common/Input/NumberInput'; -import { ChatItemContext } from '@/web/core/chat/context/chatItemContext'; -import { ChatBoxContext } from '../Provider'; -import dynamic from 'next/dynamic'; - -const JsonEditor = dynamic(() => import('@fastgpt/web/components/common/Textarea/JsonEditor')); - -export const VariableInputItem = ({ - item, - variablesForm -}: { - item: VariableItemType; - variablesForm: UseFormReturn; -}) => { - const { - control, - setValue, - formState: { errors } - } = variablesForm; - - return ( - - - {item.label} - {item.required && ( - - * - - )} - {item.description && } - - - { - if (item.type === VariableInputEnum.input) { - return ( - - ); - } - if (item.type === VariableInputEnum.select) { - return ( - ({ - label: item.value, - value: item.value - }))} - value={value} - onChange={(e) => setValue(`variables.${item.key}`, e)} - /> - ); - } - if (item.type === VariableInputEnum.numberInput) { - return ( - - ); - } - return ( -