4.8.5 test (#1805)

* perf: revert tip

* feat: create copy app

* perf: file stream read

* perf: read directory over 100 files

* perf: index

* fix: team chat api error

* lock

* fix: i18n file
This commit is contained in:
Archer 2024-06-21 10:09:00 +08:00 committed by GitHub
parent 980b4d3db5
commit 5cc01b8509
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
57 changed files with 8660 additions and 10755 deletions

View File

@ -4,7 +4,7 @@
"typescript.tsdk": "node_modules/typescript/lib", "typescript.tsdk": "node_modules/typescript/lib",
"prettier.prettierPath": "", "prettier.prettierPath": "",
"i18n-ally.localesPaths": [ "i18n-ally.localesPaths": [
"projects/app/i18n", "packages/web/i18n",
], ],
"i18n-ally.enabledParsers": ["json", "yaml", "js", "ts"], "i18n-ally.enabledParsers": ["json", "yaml", "js", "ts"],
"i18n-ally.keystyle": "nested", "i18n-ally.keystyle": "nested",

View File

@ -32,6 +32,10 @@ curl --location --request POST 'https://{{host}}/api/admin/initv485' \
## V4.8.5 更新说明 ## V4.8.5 更新说明
1. 新增 - 合并插件和应用,统一成工作台 1. 新增 - 合并插件和应用,统一成工作台
2. 修复 - SSR渲染 2. 新增 - 应用创建副本功能
3. 修复 - 定时任务无法实际关闭 3. 优化 - 原文件编码存取
4. 修复 - 输入引导特殊字符导致正则报错 4. 优化 - 文件夹读取,支持单个文件夹超出 100 个文件
5. 优化 - 问答拆分/手动录入,当有`a`字段时,自动将`q`作为补充索引。
6. 修复 - SSR渲染
7. 修复 - 定时任务无法实际关闭
8. 修复 - 输入引导特殊字符导致正则报错

View File

@ -21,12 +21,6 @@
"react-i18next": "13.5.0", "react-i18next": "13.5.0",
"zhlint": "^0.7.1" "zhlint": "^0.7.1"
}, },
"resolutions": {
"react": "18.3.1",
"react-dom": "18.3.1",
"@types/react": "18.3.0",
"@types/react-dom": "18.3.0"
},
"lint-staged": { "lint-staged": {
"./**/**/*.{ts,tsx,scss}": "npm run format-code", "./**/**/*.{ts,tsx,scss}": "npm run format-code",
"./docSite/**/**/*.md": "npm run format-doc" "./docSite/**/**/*.md": "npm run format-doc"

View File

@ -350,7 +350,7 @@ export const splitText2Chunks = (props: SplitProps): SplitResponse => {
return commonSplit(props); return commonSplit(props);
}); });
console.log(Date.now() - start);
return { return {
chunks: splitResult.map((item) => item.chunks).flat(), chunks: splitResult.map((item) => item.chunks).flat(),
chars: splitResult.reduce((sum, item) => sum + item.chars, 0) chars: splitResult.reduce((sum, item) => sum + item.chars, 0)

View File

@ -179,10 +179,9 @@ export type DatasetFileSchema = {
filename: string; filename: string;
contentType: string; contentType: string;
metadata: { metadata: {
contentType: string;
datasetId: string;
teamId: string; teamId: string;
tmbId: string; tmbId: string;
encoding?: string;
}; };
}; };

View File

@ -17,6 +17,6 @@
}, },
"devDependencies": { "devDependencies": {
"@types/js-yaml": "^4.0.9", "@types/js-yaml": "^4.0.9",
"@types/node": "^20.8.5" "@types/node": "^20.14.2"
} }
} }

View File

@ -3,7 +3,7 @@
"version": "1.0.0", "version": "1.0.0",
"dependencies": {}, "dependencies": {},
"devDependencies": { "devDependencies": {
"@types/node": "^20.8.5", "@types/node": "^20.14.2",
"@fastgpt/global": "workspace:*", "@fastgpt/global": "workspace:*",
"@fastgpt/service": "workspace:*" "@fastgpt/service": "workspace:*"
} }

View File

@ -8,7 +8,8 @@ import { detectFileEncoding } from '@fastgpt/global/common/file/tools';
import { CommonErrEnum } from '@fastgpt/global/common/error/code/common'; import { CommonErrEnum } from '@fastgpt/global/common/error/code/common';
import { MongoRawTextBuffer } from '../../buffer/rawText/schema'; import { MongoRawTextBuffer } from '../../buffer/rawText/schema';
import { readRawContentByFileBuffer } from '../read/utils'; import { readRawContentByFileBuffer } from '../read/utils';
import { gridFsStream2Buffer } from './utils'; import { gridFsStream2Buffer, stream2Encoding } from './utils';
import { addLog } from '../../system/log';
export function getGFSCollection(bucket: `${BucketNameEnum}`) { export function getGFSCollection(bucket: `${BucketNameEnum}`) {
MongoFileSchema; MongoFileSchema;
@ -44,8 +45,11 @@ export async function uploadFile({
const stats = await fsp.stat(path); const stats = await fsp.stat(path);
if (!stats.isFile()) return Promise.reject(`${path} is not a file`); if (!stats.isFile()) return Promise.reject(`${path} is not a file`);
const { stream: readStream, encoding } = await stream2Encoding(fs.createReadStream(path));
metadata.teamId = teamId; metadata.teamId = teamId;
metadata.tmbId = tmbId; metadata.tmbId = tmbId;
metadata.encoding = encoding;
// create a gridfs bucket // create a gridfs bucket
const bucket = getGridBucket(bucketName); const bucket = getGridBucket(bucketName);
@ -57,7 +61,7 @@ export async function uploadFile({
// save to gridfs // save to gridfs
await new Promise((resolve, reject) => { await new Promise((resolve, reject) => {
fs.createReadStream(path) readStream
.pipe(stream as any) .pipe(stream as any)
.on('finish', resolve) .on('finish', resolve)
.on('error', reject); .on('error', reject);
@ -113,19 +117,8 @@ export async function getDownloadStream({
fileId: string; fileId: string;
}) { }) {
const bucket = getGridBucket(bucketName); const bucket = getGridBucket(bucketName);
const encodeStream = bucket.openDownloadStream(new Types.ObjectId(fileId), { end: 100 });
const rawStream = bucket.openDownloadStream(new Types.ObjectId(fileId));
/* get encoding */ return bucket.openDownloadStream(new Types.ObjectId(fileId));
const buffer = await gridFsStream2Buffer(encodeStream);
const encoding = detectFileEncoding(buffer);
return {
fileStream: rawStream,
encoding
// encoding: 'utf-8'
};
} }
export const readFileContentFromMongo = async ({ export const readFileContentFromMongo = async ({
@ -150,9 +143,8 @@ export const readFileContentFromMongo = async ({
filename: fileBuffer.metadata?.filename || '' filename: fileBuffer.metadata?.filename || ''
}; };
} }
const start = Date.now();
const [file, { encoding, fileStream }] = await Promise.all([ const [file, fileStream] = await Promise.all([
getFileById({ bucketName, fileId }), getFileById({ bucketName, fileId }),
getDownloadStream({ bucketName, fileId }) getDownloadStream({ bucketName, fileId })
]); ]);
@ -163,8 +155,11 @@ export const readFileContentFromMongo = async ({
const extension = file?.filename?.split('.')?.pop()?.toLowerCase() || ''; const extension = file?.filename?.split('.')?.pop()?.toLowerCase() || '';
const start = Date.now();
const fileBuffers = await gridFsStream2Buffer(fileStream); const fileBuffers = await gridFsStream2Buffer(fileStream);
// console.log('get file buffer', Date.now() - start); addLog.debug('get file buffer', { time: Date.now() - start });
const encoding = file?.metadata?.encoding || detectFileEncoding(fileBuffers);
const { rawText } = await readRawContentByFileBuffer({ const { rawText } = await readRawContentByFileBuffer({
extension, extension,
@ -177,7 +172,8 @@ export const readFileContentFromMongo = async ({
} }
}); });
if (rawText.trim()) { // < 14M
if (fileBuffers.length < 14 * 1024 * 1024 && rawText.trim()) {
MongoRawTextBuffer.create({ MongoRawTextBuffer.create({
sourceId: fileId, sourceId: fileId,
rawText, rawText,

View File

@ -1,3 +1,6 @@
import { detectFileEncoding } from '@fastgpt/global/common/file/tools';
import { PassThrough } from 'stream';
export const gridFsStream2Buffer = (stream: NodeJS.ReadableStream) => { export const gridFsStream2Buffer = (stream: NodeJS.ReadableStream) => {
return new Promise<Buffer>((resolve, reject) => { return new Promise<Buffer>((resolve, reject) => {
let tmpBuffer: Buffer = Buffer.from([]); let tmpBuffer: Buffer = Buffer.from([]);
@ -13,3 +16,38 @@ export const gridFsStream2Buffer = (stream: NodeJS.ReadableStream) => {
}); });
}); });
}; };
export const stream2Encoding = async (stream: NodeJS.ReadableStream) => {
const start = Date.now();
const copyStream = stream.pipe(new PassThrough());
/* get encoding */
const buffer = await (() => {
return new Promise<Buffer>((resolve, reject) => {
let tmpBuffer: Buffer = Buffer.from([]);
stream.on('data', (chunk) => {
if (tmpBuffer.length < 200) {
tmpBuffer = Buffer.concat([tmpBuffer, chunk]);
if (tmpBuffer.length >= 200) {
resolve(tmpBuffer);
}
}
});
stream.on('end', () => {
resolve(tmpBuffer);
});
stream.on('error', (err) => {
reject(err);
});
});
})();
const enc = detectFileEncoding(buffer);
console.log('Get encoding time', Date.now() - start, enc);
return {
encoding: enc,
stream: copyStream
};
};

View File

@ -15,12 +15,12 @@
"decompress": "^4.2.1", "decompress": "^4.2.1",
"domino-ext": "^2.1.4", "domino-ext": "^2.1.4",
"encoding": "^0.1.13", "encoding": "^0.1.13",
"lodash": "^4.17.21",
"file-type": "^19.0.0", "file-type": "^19.0.0",
"iconv-lite": "^0.6.3", "iconv-lite": "^0.6.3",
"joplin-turndown-plugin-gfm": "^1.0.12", "joplin-turndown-plugin-gfm": "^1.0.12",
"json5": "^2.2.3", "json5": "^2.2.3",
"jsonwebtoken": "^9.0.2", "jsonwebtoken": "^9.0.2",
"lodash": "^4.17.21",
"mammoth": "^1.6.0", "mammoth": "^1.6.0",
"mongoose": "^7.0.2", "mongoose": "^7.0.2",
"multer": "1.4.5-lts.1", "multer": "1.4.5-lts.1",
@ -39,10 +39,10 @@
"@types/cookie": "^0.5.2", "@types/cookie": "^0.5.2",
"@types/decompress": "^4.2.7", "@types/decompress": "^4.2.7",
"@types/jsonwebtoken": "^9.0.3", "@types/jsonwebtoken": "^9.0.3",
"@types/lodash": "^4.14.191",
"@types/multer": "^1.4.10", "@types/multer": "^1.4.10",
"@types/node-cron": "^3.0.11", "@types/node-cron": "^3.0.11",
"@types/papaparse": "5.3.7", "@types/papaparse": "5.3.7",
"@types/lodash": "^4.14.191",
"@types/pg": "^8.6.6", "@types/pg": "^8.6.6",
"@types/tunnel": "^0.0.4", "@types/tunnel": "^0.0.4",
"@types/turndown": "^5.0.4" "@types/turndown": "^5.0.4"

View File

@ -46,12 +46,26 @@ const MyMenu = ({
_hover: { _hover: {
backgroundColor: 'primary.50', backgroundColor: 'primary.50',
color: 'primary.600' color: 'primary.600'
},
_focus: {
backgroundColor: 'primary.50',
color: 'primary.600'
},
_active: {
backgroundColor: 'primary.50',
color: 'primary.600'
} }
}, },
danger: { danger: {
color: 'red.600', color: 'red.600',
_hover: { _hover: {
background: 'red.1' background: 'red.1'
},
_focus: {
background: 'red.1'
},
_active: {
background: 'red.1'
} }
} }
}; };

View File

@ -76,7 +76,7 @@ const PopoverConfirm = ({
<PopoverContent p={4}> <PopoverContent p={4}>
<PopoverArrow /> <PopoverArrow />
<HStack alignItems={'flex-start'}> <HStack alignItems={'flex-start'} color={'myGray.700'}>
<MyIcon name={map.icon as any} w={'1.5rem'} /> <MyIcon name={map.icon as any} w={'1.5rem'} />
<Box fontSize={'sm'}>{content}</Box> <Box fontSize={'sm'}>{content}</Box>
</HStack> </HStack>

View File

@ -9,11 +9,14 @@
"Chat Logs Tips": "Logs will record online, shared and API (chatId required) conversation records for this app", "Chat Logs Tips": "Logs will record online, shared and API (chatId required) conversation records for this app",
"Chat logs": "Chat Logs", "Chat logs": "Chat Logs",
"Confirm Del App Tip": "Confirm to delete this app and all its chat records?", "Confirm Del App Tip": "Confirm to delete this app and all its chat records?",
"Confirm copy app tip": "The system will create an application with the same configuration for you, but the permission will not be copied, please confirm!",
"Confirm delete folder tip": "Are you sure to delete this folder? All the following applications and corresponding chat records will be deleted, please confirm!", "Confirm delete folder tip": "Are you sure to delete this folder? All the following applications and corresponding chat records will be deleted, please confirm!",
"Connection is invalid": "Connection is invalid", "Connection is invalid": "Connection is invalid",
"Connection type is different": "Connection type is different", "Connection type is different": "Connection type is different",
"Copy Module Config": "Copy Config", "Copy Module Config": "Copy Config",
"Copy one app": "Copy",
"Create bot": "App", "Create bot": "App",
"Create copy success": "Create copy success",
"Create one ai app": "Create AI app", "Create one ai app": "Create AI app",
"Current settings": "Current settings", "Current settings": "Current settings",
"Dataset Quote Template": "Knowledge Base QA Mode", "Dataset Quote Template": "Knowledge Base QA Mode",
@ -77,5 +80,8 @@
"Plugin": "Plugin", "Plugin": "Plugin",
"Simple bot": "Simple bot", "Simple bot": "Simple bot",
"Workflow bot": "Workflow" "Workflow bot": "Workflow"
},
"version": {
"Revert success": "Revert success"
} }
} }

View File

@ -8,11 +8,14 @@
"Chat Logs Tips": "日志会记录该应用的在线、分享和 API(需填写 chatId) 对话记录", "Chat Logs Tips": "日志会记录该应用的在线、分享和 API(需填写 chatId) 对话记录",
"Chat logs": "对话日志", "Chat logs": "对话日志",
"Confirm Del App Tip": "确认删除该应用及其所有聊天记录?", "Confirm Del App Tip": "确认删除该应用及其所有聊天记录?",
"Confirm copy app tip": "系统将为您创建一个相同配置应用,但权限不会进行复制,请确认!",
"Confirm delete folder tip": "确认删除该文件夹?将会删除它下面所有应用及对应的聊天记录,请确认!", "Confirm delete folder tip": "确认删除该文件夹?将会删除它下面所有应用及对应的聊天记录,请确认!",
"Connection is invalid": "连接无效", "Connection is invalid": "连接无效",
"Connection type is different": "连接的类型不一致", "Connection type is different": "连接的类型不一致",
"Copy Module Config": "复制配置", "Copy Module Config": "复制配置",
"Copy one app": "创建副本",
"Create bot": "应用", "Create bot": "应用",
"Create copy success": "创建副本成功",
"Create one ai app": "创建一个AI应用", "Create one ai app": "创建一个AI应用",
"Current settings": "当前配置", "Current settings": "当前配置",
"Dataset Quote Template": "知识库问答模式", "Dataset Quote Template": "知识库问答模式",
@ -76,5 +79,8 @@
"Plugin": "插件", "Plugin": "插件",
"Simple bot": "简易应用", "Simple bot": "简易应用",
"Workflow bot": "工作流" "Workflow bot": "工作流"
},
"version": {
"Revert success": "回滚成功"
} }
} }

View File

@ -252,6 +252,7 @@
"Publish Confirm": "确认发布应用?会立即更新所有发布渠道的应用状态。", "Publish Confirm": "确认发布应用?会立即更新所有发布渠道的应用状态。",
"Publish Failed": "发布失败", "Publish Failed": "发布失败",
"Publish Success": "发布成功", "Publish Success": "发布成功",
"Publish app tip": "发布应用后,所有发布渠道将会立即使用该版本",
"Question Guide": "猜你想问", "Question Guide": "猜你想问",
"Question Guide Tip": "对话结束后,会为生成 3 个引导性问题。", "Question Guide Tip": "对话结束后,会为生成 3 个引导性问题。",
"Quote prompt": "引用模板提示词", "Quote prompt": "引用模板提示词",

18698
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -9,6 +9,7 @@ module.exports = {
locales: ['en', 'zh'], locales: ['en', 'zh'],
localeDetection: false localeDetection: false
}, },
localePath: typeof window === 'undefined' ? require('path').resolve('./i18n') : '/i18n', localePath:
typeof window === 'undefined' ? require('path').resolve('../../packages/web/i18n') : '/i18n',
reloadOnPrerender: process.env.NODE_ENV === 'development' reloadOnPrerender: process.env.NODE_ENV === 'development'
}; };

View File

@ -1,6 +1,6 @@
{ {
"name": "app", "name": "app",
"version": "4.8.4", "version": "4.8.5",
"private": false, "private": false,
"scripts": { "scripts": {
"dev": "next dev", "dev": "next dev",
@ -71,12 +71,12 @@
"@types/js-yaml": "^4.0.9", "@types/js-yaml": "^4.0.9",
"@types/jsonwebtoken": "^9.0.3", "@types/jsonwebtoken": "^9.0.3",
"@types/lodash": "^4.14.191", "@types/lodash": "^4.14.191",
"@types/node": "^20.8.5", "@types/node": "^20.14.2",
"@types/react": "18.3.0", "@types/react": "18.3.0",
"@types/react-dom": "18.3.0", "@types/react-dom": "18.3.0",
"@types/react-syntax-highlighter": "^15.5.6", "@types/react-syntax-highlighter": "^15.5.6",
"@types/request-ip": "^0.0.37", "@types/request-ip": "^0.0.37",
"eslint": "8.34.0", "eslint": "8.56.0",
"eslint-config-next": "14.2.3", "eslint-config-next": "14.2.3",
"nextjs-node-loader": "^1.1.5", "nextjs-node-loader": "^1.1.5",
"typescript": "4.9.5" "typescript": "4.9.5"

View File

@ -1,4 +1,4 @@
### FastGPT V4.8.4 ### FastGPT V4.8.5
1. 新增 - 应用使用新权限系统。 1. 新增 - 应用使用新权限系统。
2. 新增 - 应用支持文件夹。 2. 新增 - 应用支持文件夹。

View File

@ -4,6 +4,7 @@ import { connectToDatabase } from '@/service/mongo';
import { authFileToken } from '@fastgpt/service/support/permission/controller'; import { authFileToken } from '@fastgpt/service/support/permission/controller';
import { getDownloadStream, getFileById } from '@fastgpt/service/common/file/gridfs/controller'; import { getDownloadStream, getFileById } from '@fastgpt/service/common/file/gridfs/controller';
import { CommonErrEnum } from '@fastgpt/global/common/error/code/common'; import { CommonErrEnum } from '@fastgpt/global/common/error/code/common';
import { stream2Encoding } from '@fastgpt/service/common/file/gridfs/utils';
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) { export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
try { try {
@ -17,7 +18,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
throw new Error('fileId is empty'); throw new Error('fileId is empty');
} }
const [file, { fileStream, encoding }] = await Promise.all([ const [file, fileStream] = await Promise.all([
getFileById({ bucketName, fileId }), getFileById({ bucketName, fileId }),
getDownloadStream({ bucketName, fileId }) getDownloadStream({ bucketName, fileId })
]); ]);
@ -26,16 +27,26 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
return Promise.reject(CommonErrEnum.fileNotFound); return Promise.reject(CommonErrEnum.fileNotFound);
} }
const { stream, encoding } = await (async () => {
if (file.metadata?.encoding) {
return {
stream: fileStream,
encoding: file.metadata.encoding
};
}
return stream2Encoding(fileStream);
})();
res.setHeader('Content-Type', `${file.contentType}; charset=${encoding}`); res.setHeader('Content-Type', `${file.contentType}; charset=${encoding}`);
res.setHeader('Cache-Control', 'public, max-age=3600'); res.setHeader('Cache-Control', 'public, max-age=3600');
res.setHeader('Content-Disposition', `inline; filename="${encodeURIComponent(file.filename)}"`); res.setHeader('Content-Disposition', `inline; filename="${encodeURIComponent(file.filename)}"`);
fileStream.pipe(res); stream.pipe(res);
fileStream.on('error', () => { stream.on('error', () => {
res.status(500).end(); res.status(500).end();
}); });
fileStream.on('end', () => { stream.on('end', () => {
res.end(); res.end();
}); });
} catch (error) { } catch (error) {
@ -47,6 +58,6 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
} }
export const config = { export const config = {
api: { api: {
responseLimit: '32mb' responseLimit: '100mb'
} }
}; };

View File

@ -0,0 +1,50 @@
import type { ApiRequestProps, ApiResponseType } from '@fastgpt/service/type/next';
import { NextAPI } from '@/service/middleware/entry';
import { authApp } from '@fastgpt/service/support/permission/app/auth';
import { WritePermissionVal } from '@fastgpt/global/support/permission/constant';
import { authUserPer } from '@fastgpt/service/support/permission/user/auth';
import { onCreateApp } from './create';
export type copyAppQuery = {};
export type copyAppBody = { appId: string };
export type copyAppResponse = {
appId: string;
};
async function handler(
req: ApiRequestProps<copyAppBody, copyAppQuery>,
res: ApiResponseType<any>
): Promise<copyAppResponse> {
const [{ app, tmbId }] = await Promise.all([
authApp({
req,
authToken: true,
per: WritePermissionVal,
appId: req.body.appId
}),
authUserPer({
req,
authToken: true,
per: WritePermissionVal
})
]);
const appId = await onCreateApp({
parentId: app.parentId,
name: app.name + ' Copy',
intro: app.intro,
avatar: app.avatar,
type: app.type,
modules: app.modules,
edges: app.edges,
teamId: app.teamId,
tmbId,
pluginData: app.pluginData
});
return { appId };
}
export default NextAPI(handler);

View File

@ -42,27 +42,22 @@ async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
const { nodes: formatNodes } = beforeUpdateAppFormat({ nodes }); const { nodes: formatNodes } = beforeUpdateAppFormat({ nodes });
// 更新模型 // 更新模型
await MongoApp.updateOne( await MongoApp.findByIdAndUpdate(appId, {
{ ...parseParentIdInMongo(parentId),
_id: appId ...(name && { name }),
}, ...(type && { type }),
{ ...(avatar && { avatar }),
...parseParentIdInMongo(parentId), ...(intro !== undefined && { intro }),
name, ...(defaultPermission && { defaultPermission }),
type, ...(teamTags && { teamTags }),
avatar, ...(formatNodes && {
intro, modules: formatNodes
defaultPermission, }),
...(teamTags && teamTags), ...(edges && {
...(formatNodes && { edges
modules: formatNodes }),
}), ...(chatConfig && { chatConfig })
...(edges && { });
edges
}),
...(chatConfig && { chatConfig })
}
);
} }
export default NextAPI(handler); export default NextAPI(handler);

View File

@ -1,50 +1,42 @@
import type { NextApiRequest, NextApiResponse } from 'next'; import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@fastgpt/service/common/response'; import { jsonRes } from '@fastgpt/service/common/response';
import { connectToDatabase } from '@/service/mongo';
import { MongoChat } from '@fastgpt/service/core/chat/chatSchema'; import { MongoChat } from '@fastgpt/service/core/chat/chatSchema';
import { MongoChatItem } from '@fastgpt/service/core/chat/chatItemSchema'; import { MongoChatItem } from '@fastgpt/service/core/chat/chatItemSchema';
import { DelHistoryProps } from '@/global/core/chat/api'; import { DelHistoryProps } from '@/global/core/chat/api';
import { autChatCrud } from '@/service/support/permission/auth/chat'; import { autChatCrud } from '@/service/support/permission/auth/chat';
import { mongoSessionRun } from '@fastgpt/service/common/mongo/sessionRun'; import { mongoSessionRun } from '@fastgpt/service/common/mongo/sessionRun';
import { NextAPI } from '@/service/middleware/entry';
import { ApiRequestProps } from '@fastgpt/service/type/next';
/* clear chat history */ /* clear chat history */
export default async function handler(req: NextApiRequest, res: NextApiResponse) { async function handler(req: ApiRequestProps<{}, DelHistoryProps>, res: NextApiResponse) {
try { const { appId, chatId } = req.query;
await connectToDatabase();
const { appId, chatId, shareId, outLinkUid } = req.query as DelHistoryProps;
await autChatCrud({ await autChatCrud({
req, req,
authToken: true, authToken: true,
appId, ...req.query,
chatId, per: 'w'
shareId, });
outLinkUid,
per: 'w'
});
await mongoSessionRun(async (session) => { await mongoSessionRun(async (session) => {
await MongoChatItem.deleteMany( await MongoChatItem.deleteMany(
{ {
appId, appId,
chatId chatId
}, },
{ session } { session }
); );
await MongoChat.findOneAndRemove( await MongoChat.findOneAndRemove(
{ {
appId, appId,
chatId chatId
}, },
{ session } { session }
); );
}); });
jsonRes(res); jsonRes(res);
} catch (err) {
jsonRes(res, {
code: 500,
error: err
});
}
} }
export default NextAPI(handler);

View File

@ -1,42 +0,0 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@fastgpt/service/common/response';
import { connectToDatabase } from '@/service/mongo';
import { MongoChat } from '@fastgpt/service/core/chat/chatSchema';
import { ChatSourceEnum } from '@fastgpt/global/core/chat/constants';
/* clear chat history */
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try {
await connectToDatabase();
const { outLinkUid, chatIds } = req.body as {
outLinkUid: string;
chatIds: string[];
};
if (!outLinkUid) {
throw new Error('shareId or outLinkUid is required');
}
const sliceIds = chatIds.slice(0, 50);
await MongoChat.updateMany(
{
chatId: { $in: sliceIds },
source: ChatSourceEnum.share,
outLinkUid: { $exists: false }
},
{
$set: {
outLinkUid
}
}
);
jsonRes(res);
} catch (err) {
jsonRes(res, {
code: 500,
error: err
});
}
}

View File

@ -4,37 +4,30 @@ import { connectToDatabase } from '@/service/mongo';
import { MongoChatItem } from '@fastgpt/service/core/chat/chatItemSchema'; import { MongoChatItem } from '@fastgpt/service/core/chat/chatItemSchema';
import { autChatCrud } from '@/service/support/permission/auth/chat'; import { autChatCrud } from '@/service/support/permission/auth/chat';
import type { DeleteChatItemProps } from '@/global/core/chat/api.d'; import type { DeleteChatItemProps } from '@/global/core/chat/api.d';
import { NextAPI } from '@/service/middleware/entry';
import { ApiRequestProps } from '@fastgpt/service/type/next';
export default async function handler(req: NextApiRequest, res: NextApiResponse) { async function handler(req: ApiRequestProps<{}, DeleteChatItemProps>, res: NextApiResponse) {
try { const { appId, chatId, contentId, shareId, outLinkUid } = req.query;
await connectToDatabase();
const { appId, chatId, contentId, shareId, outLinkUid } = req.query as DeleteChatItemProps;
if (!contentId || !chatId) { if (!contentId || !chatId) {
return jsonRes(res); return jsonRes(res);
}
await autChatCrud({
req,
authToken: true,
appId,
chatId,
shareId,
outLinkUid,
per: 'w'
});
await MongoChatItem.deleteOne({
appId,
chatId,
dataId: contentId
});
jsonRes(res);
} catch (err) {
jsonRes(res, {
code: 500,
error: err
});
} }
await autChatCrud({
req,
authToken: true,
...req.query,
per: 'w'
});
await MongoChatItem.deleteOne({
appId,
chatId,
dataId: contentId
});
jsonRes(res);
} }
export default NextAPI(handler);

View File

@ -1,40 +1,30 @@
import type { NextApiRequest, NextApiResponse } from 'next'; import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@fastgpt/service/common/response'; import { jsonRes } from '@fastgpt/service/common/response';
import { connectToDatabase } from '@/service/mongo';
import { UpdateHistoryProps } from '@/global/core/chat/api.d'; import { UpdateHistoryProps } from '@/global/core/chat/api.d';
import { MongoChat } from '@fastgpt/service/core/chat/chatSchema'; import { MongoChat } from '@fastgpt/service/core/chat/chatSchema';
import { autChatCrud } from '@/service/support/permission/auth/chat'; import { autChatCrud } from '@/service/support/permission/auth/chat';
import { NextAPI } from '@/service/middleware/entry';
import { ApiRequestProps } from '@fastgpt/service/type/next';
/* update chat top, custom title */ /* update chat top, custom title */
export default async function handler(req: NextApiRequest, res: NextApiResponse) { async function handler(req: ApiRequestProps<UpdateHistoryProps>, res: NextApiResponse) {
try { const { appId, chatId, customTitle, top } = req.body;
await connectToDatabase(); await autChatCrud({
const { appId, chatId, teamId, shareId, outLinkUid, customTitle, top } = req,
req.body as UpdateHistoryProps; authToken: true,
await autChatCrud({ ...req.body,
req, per: 'w'
authToken: true, });
appId,
teamId,
chatId,
shareId,
outLinkUid,
per: 'w'
});
await MongoChat.findOneAndUpdate( await MongoChat.findOneAndUpdate(
{ appId, chatId }, { appId, chatId },
{ {
updateTime: new Date(), updateTime: new Date(),
...(customTitle !== undefined && { customTitle }), ...(customTitle !== undefined && { customTitle }),
...(top !== undefined && { top }) ...(top !== undefined && { top })
} }
); );
jsonRes(res); jsonRes(res);
} catch (err) {
jsonRes(res, {
code: 500,
error: err
});
}
} }
export default NextAPI(handler);

View File

@ -3,18 +3,17 @@ import { getPublishList, postRevertVersion } from '@/web/core/app/api/version';
import { useScrollPagination } from '@fastgpt/web/hooks/useScrollPagination'; import { useScrollPagination } from '@fastgpt/web/hooks/useScrollPagination';
import CustomRightDrawer from '@fastgpt/web/components/common/MyDrawer/CustomRightDrawer'; import CustomRightDrawer from '@fastgpt/web/components/common/MyDrawer/CustomRightDrawer';
import { useTranslation } from 'next-i18next'; import { useTranslation } from 'next-i18next';
import { useMemoizedFn } from 'ahooks';
import { Box, Button, Flex } from '@chakra-ui/react'; import { Box, Button, Flex } from '@chakra-ui/react';
import { formatTime2YMDHM } from '@fastgpt/global/common/string/time'; import { formatTime2YMDHM } from '@fastgpt/global/common/string/time';
import { useContextSelector } from 'use-context-selector'; import { useContextSelector } from 'use-context-selector';
import { AppVersionSchemaType } from '@fastgpt/global/core/app/version'; import { AppVersionSchemaType } from '@fastgpt/global/core/app/version';
import MyIcon from '@fastgpt/web/components/common/Icon'; import MyIcon from '@fastgpt/web/components/common/Icon';
import MyTooltip from '@fastgpt/web/components/common/MyTooltip'; import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
import { useConfirm } from '@fastgpt/web/hooks/useConfirm'; import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { useRequest } from '@fastgpt/web/hooks/useRequest';
import { AppContext } from './context'; import { AppContext } from './context';
import { useI18n } from '@/web/context/I18n'; import { useI18n } from '@/web/context/I18n';
import { AppSchema } from '@fastgpt/global/core/app/type'; import { AppSchema } from '@fastgpt/global/core/app/type';
import PopoverConfirm from '@fastgpt/web/components/common/MyPopover/PopoverConfirm';
export type InitProps = { export type InitProps = {
nodes: AppSchema['modules']; nodes: AppSchema['modules'];
@ -33,11 +32,11 @@ const PublishHistoriesSlider = ({
}) => { }) => {
const { t } = useTranslation(); const { t } = useTranslation();
const { appT } = useI18n(); const { appT } = useI18n();
const { openConfirm, ConfirmModal } = useConfirm({
content: t('core.workflow.publish.OnRevert version confirm')
});
const { appDetail, setAppDetail } = useContextSelector(AppContext, (v) => v); const { appDetail, setAppDetail, reloadAppLatestVersion } = useContextSelector(
AppContext,
(v) => v
);
const appId = appDetail._id; const appId = appDetail._id;
const [selectedHistoryId, setSelectedHistoryId] = useState<string>(); const [selectedHistoryId, setSelectedHistoryId] = useState<string>();
@ -73,8 +72,8 @@ const PublishHistoriesSlider = ({
[initData, onClose] [initData, onClose]
); );
const { mutate: onRevert, isLoading: isReverting } = useRequest({ const { runAsync: onRevert } = useRequest2(
mutationFn: async (data: AppVersionSchemaType) => { async (data: AppVersionSchemaType) => {
if (!appId) return; if (!appId) return;
await postRevertVersion(appId, { await postRevertVersion(appId, {
versionId: data._id, versionId: data._id,
@ -90,10 +89,14 @@ const PublishHistoriesSlider = ({
})); }));
onCloseSlider(data); onCloseSlider(data);
reloadAppLatestVersion();
},
{
successToast: appT('version.Revert success')
} }
}); );
const showLoading = isLoading || isReverting; const showLoading = isLoading;
return ( return (
<> <>
@ -173,24 +176,28 @@ const PublishHistoriesSlider = ({
{formatTime2YMDHM(item.time)} {formatTime2YMDHM(item.time)}
</Box> </Box>
{item._id === selectedHistoryId && ( {item._id === selectedHistoryId && (
<MyTooltip label={t('core.workflow.publish.OnRevert version')}> <PopoverConfirm
<MyIcon showCancel
name={'core/workflow/revertVersion'} content={t('core.workflow.publish.OnRevert version confirm')}
w={'20px'} onConfirm={() => onRevert(item)}
color={'primary.600'} Trigger={
onClick={(e) => { <Box>
e.stopPropagation(); <MyTooltip label={t('core.workflow.publish.OnRevert version')}>
openConfirm(() => onRevert(item))(); <MyIcon
}} name={'core/workflow/revertVersion'}
/> w={'20px'}
</MyTooltip> color={'primary.600'}
/>
</MyTooltip>
</Box>
}
/>
)} )}
</Flex> </Flex>
); );
})} })}
</ScrollList> </ScrollList>
</CustomRightDrawer> </CustomRightDrawer>
<ConfirmModal />
</> </>
); );
}; };

View File

@ -98,7 +98,6 @@ const AppCard = () => {
</Button> </Button>
{appDetail.permission.hasWritePer && feConfigs?.show_team_chat && ( {appDetail.permission.hasWritePer && feConfigs?.show_team_chat && (
<Button <Button
mr={3}
size={['sm', 'md']} size={['sm', 'md']}
variant={'whitePrimary'} variant={'whitePrimary'}
leftIcon={<DragHandleIcon w={'16px'} />} leftIcon={<DragHandleIcon w={'16px'} />}

View File

@ -20,6 +20,7 @@ import MyIcon from '@fastgpt/web/components/common/Icon';
import { compareWorkflow } from '@/web/core/workflow/utils'; import { compareWorkflow } from '@/web/core/workflow/utils';
import MyTag from '@fastgpt/web/components/common/Tag/index'; import MyTag from '@fastgpt/web/components/common/Tag/index';
import { publishStatusStyle } from '../constants'; import { publishStatusStyle } from '../constants';
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
const Header = ({ const Header = ({
appForm, appForm,
@ -134,7 +135,13 @@ const Header = ({
<PopoverConfirm <PopoverConfirm
showCancel showCancel
content={t('core.app.Publish Confirm')} content={t('core.app.Publish Confirm')}
Trigger={<Button isDisabled={isPublished}>{t('core.app.Publish')}</Button>} Trigger={
<Box>
<MyTooltip label={t('core.app.Publish app tip')}>
<Button isDisabled={isPublished}>{t('core.app.Publish')}</Button>
</MyTooltip>
</Box>
}
onConfirm={() => onSubmitPublish(appForm)} onConfirm={() => onSubmitPublish(appForm)}
/> />
</> </>

View File

@ -7,15 +7,22 @@ import { useContextSelector } from 'use-context-selector';
import { AppContext, TabEnum } from '../context'; import { AppContext, TabEnum } from '../context';
import dynamic from 'next/dynamic'; import dynamic from 'next/dynamic';
import { Flex } from '@chakra-ui/react'; import { Flex } from '@chakra-ui/react';
import { useBeforeunload } from '@fastgpt/web/hooks/useBeforeunload';
import { useTranslation } from 'next-i18next';
const Logs = dynamic(() => import('../Logs/index')); const Logs = dynamic(() => import('../Logs/index'));
const PublishChannel = dynamic(() => import('../Publish')); const PublishChannel = dynamic(() => import('../Publish'));
const SimpleEdit = () => { const SimpleEdit = () => {
const { t } = useTranslation();
const { currentTab } = useContextSelector(AppContext, (v) => v); const { currentTab } = useContextSelector(AppContext, (v) => v);
const [appForm, setAppForm] = useState(getDefaultAppForm()); const [appForm, setAppForm] = useState(getDefaultAppForm());
useBeforeunload({
tip: t('core.common.tip.leave page')
});
return ( return (
<Flex h={'100%'} flexDirection={'column'} pr={3} pb={3}> <Flex h={'100%'} flexDirection={'column'} pr={3} pb={3}>
<Header appForm={appForm} setAppForm={setAppForm} /> <Header appForm={appForm} setAppForm={setAppForm} />

View File

@ -16,6 +16,7 @@ import { useRouter } from 'next/router';
import AppCard from '../WorkflowComponents/AppCard'; import AppCard from '../WorkflowComponents/AppCard';
import { uiWorkflow2StoreWorkflow } from '../WorkflowComponents/utils'; import { uiWorkflow2StoreWorkflow } from '../WorkflowComponents/utils';
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
const PublishHistories = dynamic(() => import('../PublishHistoriesSlider')); const PublishHistories = dynamic(() => import('../PublishHistoriesSlider'));
const Header = () => { const Header = () => {
@ -53,6 +54,7 @@ const Header = () => {
router.push('/app/list'); router.push('/app/list');
} catch (error) {} } catch (error) {}
}, [onSaveWorkflow, router]); }, [onSaveWorkflow, router]);
// effect // effect
useBeforeunload({ useBeforeunload({
callback: onSaveWorkflow, callback: onSaveWorkflow,
@ -152,13 +154,17 @@ const Header = () => {
showCancel showCancel
content={t('core.app.Publish Confirm')} content={t('core.app.Publish Confirm')}
Trigger={ Trigger={
<Button <Box>
ml={[2, 4]} <MyTooltip label={t('core.app.Publish app tip')}>
size={'sm'} <Button
leftIcon={<MyIcon name={'common/publishFill'} w={['14px', '16px']} />} ml={[2, 4]}
> size={'sm'}
{t('core.app.Publish')} leftIcon={<MyIcon name={'common/publishFill'} w={['14px', '16px']} />}
</Button> >
{t('core.app.Publish')}
</Button>
</MyTooltip>
</Box>
} }
onConfirm={() => onclickPublish()} onConfirm={() => onclickPublish()}
/> />

View File

@ -520,6 +520,7 @@ const WorkflowContextProvider = ({
historiesDefaultData || historiesDefaultData ||
isSaving || isSaving ||
nodes.length === 0 || nodes.length === 0 ||
edges.length === 0 ||
!!workflowDebugData !!workflowDebugData
) )
return; return;

View File

@ -44,6 +44,7 @@ type AppContextType = {
chatConfig: AppChatConfigType; chatConfig: AppChatConfigType;
} }
| undefined; | undefined;
reloadAppLatestVersion: () => void;
}; };
export const AppContext = createContext<AppContextType>({ export const AppContext = createContext<AppContextType>({
@ -72,7 +73,10 @@ export const AppContext = createContext<AppContextType>({
onPublish: function (data: PostPublishAppProps): Promise<void> { onPublish: function (data: PostPublishAppProps): Promise<void> {
throw new Error('Function not implemented.'); throw new Error('Function not implemented.');
}, },
appLatestVersion: undefined appLatestVersion: undefined,
reloadAppLatestVersion: function (): void {
throw new Error('Function not implemented.');
}
}); });
const AppContextProvider = ({ children }: { children: ReactNode }) => { const AppContextProvider = ({ children }: { children: ReactNode }) => {
@ -190,7 +194,8 @@ const AppContextProvider = ({ children }: { children: ReactNode }) => {
onOpenTeamTagModal, onOpenTeamTagModal,
onDelApp, onDelApp,
onPublish, onPublish,
appLatestVersion appLatestVersion,
reloadAppLatestVersion
}; };
return ( return (

View File

@ -1,5 +1,5 @@
import React, { useMemo, useState } from 'react'; import React, { useMemo, useState } from 'react';
import { Box, Grid, Flex, IconButton, border } from '@chakra-ui/react'; import { Box, Grid, Flex, IconButton } from '@chakra-ui/react';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { delAppById, putAppById } from '@/web/core/app/api'; import { delAppById, putAppById } from '@/web/core/app/api';
import { useConfirm } from '@fastgpt/web/hooks/useConfirm'; import { useConfirm } from '@fastgpt/web/hooks/useConfirm';
@ -34,16 +34,15 @@ const EditResourceModal = dynamic(() => import('@/components/common/Modal/EditRe
const ConfigPerModal = dynamic(() => import('@/components/support/permission/ConfigPerModal')); const ConfigPerModal = dynamic(() => import('@/components/support/permission/ConfigPerModal'));
import type { EditHttpPluginProps } from './HttpPluginEditModal'; import type { EditHttpPluginProps } from './HttpPluginEditModal';
import { postCopyApp } from '@/web/core/app/api/app';
const HttpEditModal = dynamic(() => import('./HttpPluginEditModal')); const HttpEditModal = dynamic(() => import('./HttpPluginEditModal'));
const ListItem = () => { const ListItem = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const { appT } = useI18n(); const { appT } = useI18n();
const router = useRouter(); const router = useRouter();
const { myApps, loadMyApps, onUpdateApp, setMoveAppId, folderDetail } = useContextSelector( const { myApps, loadMyApps, onUpdateApp, setMoveAppId, folderDetail, parentId } =
AppListContext, useContextSelector(AppListContext, (v) => v);
(v) => v
);
const [loadingAppId, setLoadingAppId] = useState<string>(); const [loadingAppId, setLoadingAppId] = useState<string>();
const [editedApp, setEditedApp] = useState<EditResourceInfoFormType>(); const [editedApp, setEditedApp] = useState<EditResourceInfoFormType>();
@ -68,27 +67,33 @@ const ListItem = () => {
} }
}); });
const { openConfirm, ConfirmModal } = useConfirm({ const { openConfirm: openConfirmDel, ConfirmModal: DelConfirmModal } = useConfirm({
type: 'delete' type: 'delete'
}); });
const { runAsync: onclickDelApp } = useRequest2(
const { run: onclickDelApp } = useRequest2(
(id: string) => { (id: string) => {
setLoadingAppId(id);
return delAppById(id); return delAppById(id);
}, },
{ {
onSuccess() { onSuccess() {
loadMyApps(); loadMyApps();
}, },
onFinally() {
setLoadingAppId(undefined);
},
successToast: t('common.Delete Success'), successToast: t('common.Delete Success'),
errorToast: t('common.Delete Failed') errorToast: t('common.Delete Failed')
} }
); );
const { openConfirm: openConfirmCopy, ConfirmModal: ConfirmCopyModal } = useConfirm({
content: appT('Confirm copy app tip')
});
const { runAsync: onclickCopy } = useRequest2(postCopyApp, {
onSuccess({ appId }) {
router.push(`/app/detail?appId=${appId}`);
loadMyApps();
},
successToast: appT('Create copy success')
});
return ( return (
<> <>
<Grid <Grid
@ -218,6 +223,16 @@ const ListItem = () => {
: []) : [])
] ]
}, },
{
children: [
{
icon: 'copy',
label: appT('Copy one app'),
onClick: () =>
openConfirmCopy(() => onclickCopy({ appId: app._id }))()
}
]
},
{ {
children: [ children: [
{ {
@ -238,7 +253,7 @@ const ListItem = () => {
icon: 'delete', icon: 'delete',
label: t('common.Delete'), label: t('common.Delete'),
onClick: () => onClick: () =>
openConfirm( openConfirmDel(
() => onclickDelApp(app._id), () => onclickDelApp(app._id),
undefined, undefined,
app.type === AppTypeEnum.folder app.type === AppTypeEnum.folder
@ -280,7 +295,9 @@ const ListItem = () => {
</Grid> </Grid>
{myApps.length === 0 && <EmptyTip text={'还没有应用,快去创建一个吧!'} pt={'30vh'} />} {myApps.length === 0 && <EmptyTip text={'还没有应用,快去创建一个吧!'} pt={'30vh'} />}
<ConfirmModal />
<DelConfirmModal />
<ConfirmCopyModal />
{!!editedApp && ( {!!editedApp && (
<EditResourceModal <EditResourceModal
{...editedApp} {...editedApp}

View File

@ -163,7 +163,7 @@ const ChatHistorySlider = ({
<Flex w={'100%'} px={[2, 5]} h={'36px'} my={5} alignItems={'center'}> <Flex w={'100%'} px={[2, 5]} h={'36px'} my={5} alignItems={'center'}>
{!isPc && appId && ( {!isPc && appId && (
<Tabs <Tabs
w={'180px'} flex={'1 0 0'}
mr={2} mr={2}
list={[ list={[
{ label: t('core.chat.Recent use'), id: TabEnum.recently }, { label: t('core.chat.Recent use'), id: TabEnum.recently },
@ -176,7 +176,7 @@ const ChatHistorySlider = ({
)} )}
<Button <Button
variant={'whitePrimary'} variant={'whitePrimary'}
flex={1} flex={['0', 1]}
h={'100%'} h={'100%'}
color={'primary.600'} color={'primary.600'}
borderRadius={'xl'} borderRadius={'xl'}

View File

@ -298,7 +298,7 @@ const OutLink = ({
onClose={onCloseSlider} onClose={onCloseSlider}
> >
<DrawerOverlay backgroundColor={'rgba(255,255,255,0.5)'} /> <DrawerOverlay backgroundColor={'rgba(255,255,255,0.5)'} />
<DrawerContent maxWidth={'250px'} boxShadow={'2px 0 10px rgba(0,0,0,0.15)'}> <DrawerContent maxWidth={'75vw'} boxShadow={'2px 0 10px rgba(0,0,0,0.15)'}>
{children} {children}
</DrawerContent> </DrawerContent>
</Drawer> </Drawer>

View File

@ -83,8 +83,8 @@ const OutLink = () => {
data: { data: {
messages: prompts, messages: prompts,
variables: { variables: {
...customVariables, ...variables,
...variables ...customVariables
}, },
appId, appId,
teamId, teamId,
@ -290,7 +290,7 @@ const OutLink = () => {
onClose={onCloseSlider} onClose={onCloseSlider}
> >
<DrawerOverlay backgroundColor={'rgba(255,255,255,0.5)'} /> <DrawerOverlay backgroundColor={'rgba(255,255,255,0.5)'} />
<DrawerContent maxWidth={'250px'}>{children}</DrawerContent> <DrawerContent maxWidth={'75vw'}>{children}</DrawerContent>
</Drawer> </Drawer>
); );
})( })(

View File

@ -170,12 +170,14 @@ const FileSelector = ({
const items = e.dataTransfer.items; const items = e.dataTransfer.items;
const fileList: SelectFileItemType[] = []; const fileList: SelectFileItemType[] = [];
if (e.dataTransfer.items.length <= 1) { const firstEntry = items[0].webkitGetAsEntry();
const traverseFileTree = async (item: any) => {
return new Promise<void>((resolve, reject) => { if (firstEntry?.isDirectory && items.length === 1) {
if (item.isFile) { {
item.file((file: File) => { const readFile = (entry: any) => {
const folderPath = (item.fullPath || '').split('/').slice(2, -1).join('/'); return new Promise((resolve) => {
entry.file((file: File) => {
const folderPath = (entry.fullPath || '').split('/').slice(2, -1).join('/');
if (filterTypeReg.test(file.name)) { if (filterTypeReg.test(file.name)) {
fileList.push({ fileList.push({
@ -184,24 +186,45 @@ const FileSelector = ({
file file
}); });
} }
resolve(); resolve(file);
}); });
} else if (item.isDirectory) { });
const dirReader = item.createReader(); };
const traverseFileTree = (dirReader: any) => {
return new Promise((resolve) => {
let fileNum = 0;
dirReader.readEntries(async (entries: any[]) => { dirReader.readEntries(async (entries: any[]) => {
for (let i = 0; i < entries.length; i++) { for await (const entry of entries) {
await traverseFileTree(entries[i]); if (entry.isFile) {
await readFile(entry);
fileNum++;
} else if (entry.isDirectory) {
await traverseFileTree(entry.createReader());
}
} }
resolve();
});
}
});
};
for await (const item of items) { // chrome: readEntries will return 100 entries at most
await traverseFileTree(item.webkitGetAsEntry()); if (fileNum === 100) {
await traverseFileTree(dirReader);
}
resolve('');
});
});
};
for await (const item of items) {
const entry = item.webkitGetAsEntry();
if (entry) {
if (entry.isFile) {
await readFile(entry);
} else if (entry.isDirectory) {
//@ts-ignore
await traverseFileTree(entry.createReader());
}
}
}
} }
} else { } else if (firstEntry?.isFile) {
const files = Array.from(e.dataTransfer.files); const files = Array.from(e.dataTransfer.files);
let isErr = files.some((item) => item.type === ''); let isErr = files.some((item) => item.type === '');
if (isErr) { if (isErr) {
@ -220,6 +243,11 @@ const FileSelector = ({
file file
})) }))
); );
} else {
return toast({
title: fileT('upload error description'),
status: 'error'
});
} }
selectFileCallback(fileList.slice(0, maxCount)); selectFileCallback(fileList.slice(0, maxCount));

View File

@ -55,6 +55,12 @@ export async function insertData2Dataset({
if (!indexes.find((index) => index.defaultIndex)) { if (!indexes.find((index) => index.defaultIndex)) {
indexes.unshift(getDefaultIndex({ q, a })); indexes.unshift(getDefaultIndex({ q, a }));
} else if (q && a && !indexes.find((index) => index.text === q)) {
// push a q index
indexes.push({
defaultIndex: false,
text: q
});
} }
indexes = indexes.slice(0, 6); indexes = indexes.slice(0, 6);

View File

@ -11,6 +11,7 @@ import { authOutLinkValid } from '@fastgpt/service/support/permission/publish/au
import { AuthUserTypeEnum, ReadPermissionVal } from '@fastgpt/global/support/permission/constant'; import { AuthUserTypeEnum, ReadPermissionVal } from '@fastgpt/global/support/permission/constant';
import { MongoTeamMember } from '@fastgpt/service/support/user/team/teamMemberSchema'; import { MongoTeamMember } from '@fastgpt/service/support/user/team/teamMemberSchema';
import { OutLinkChatAuthProps } from '@fastgpt/global/support/permission/chat'; import { OutLinkChatAuthProps } from '@fastgpt/global/support/permission/chat';
import { addLog } from '@fastgpt/service/common/system/log';
/* /*
outLink: Must be the owner outLink: Must be the owner
token: team owner and chat owner have all permissions token: team owner and chat owner have all permissions
@ -55,6 +56,7 @@ export async function autChatCrud({
// auth team space chat // auth team space chat
if (spaceTeamId && teamToken) { if (spaceTeamId && teamToken) {
const { uid } = await authTeamSpaceToken({ teamId: spaceTeamId, teamToken }); const { uid } = await authTeamSpaceToken({ teamId: spaceTeamId, teamToken });
addLog.debug('Auth team token', { uid, spaceTeamId, teamToken, chatUid: chat.outLinkUid });
if (!chat || (String(chat.teamId) === String(spaceTeamId) && chat.outLinkUid === uid)) { if (!chat || (String(chat.teamId) === String(spaceTeamId) && chat.outLinkUid === uid)) {
return { uid }; return { uid };
} }

View File

@ -1,12 +1,12 @@
import 'i18next'; import 'i18next';
import common from '../../i18n/zh/common.json'; import common from '@fastgpt/web/i18n/zh/common.json';
import dataset from '../../i18n/zh/dataset.json'; import dataset from '@fastgpt/web/i18n/zh/dataset.json';
import app from '../../i18n/zh/app.json'; import app from '@fastgpt/web/i18n/zh/app.json';
import file from '../../i18n/zh/file.json'; import file from '@fastgpt/web/i18n/zh/file.json';
import publish from '../../i18n/zh/publish.json'; import publish from '@fastgpt/web/i18n/zh/publish.json';
import workflow from '../../i18n/zh/workflow.json'; import workflow from '@fastgpt/web/i18n/zh/workflow.json';
import user from '../../i18n/zh/user.json'; import user from '@fastgpt/web/i18n/zh/user.json';
import chat from '../../i18n/zh/chat.json'; import chat from '@fastgpt/web/i18n/zh/chat.json';
export interface I18nNamespaces { export interface I18nNamespaces {
common: typeof common; common: typeof common;

View File

@ -10,7 +10,7 @@ export const postUploadFiles = (
onUploadProgress: (progressEvent: AxiosProgressEvent) => void onUploadProgress: (progressEvent: AxiosProgressEvent) => void
) => ) =>
POST<string>('/common/file/upload', data, { POST<string>('/common/file/upload', data, {
timeout: 480000, timeout: 600000,
onUploadProgress, onUploadProgress,
headers: { headers: {
'Content-Type': 'multipart/form-data; charset=utf-8' 'Content-Type': 'multipart/form-data; charset=utf-8'

View File

@ -6,6 +6,7 @@ import type {
transitionWorkflowBody, transitionWorkflowBody,
transitionWorkflowResponse transitionWorkflowResponse
} from '@/pages/api/core/app/transitionWorkflow'; } from '@/pages/api/core/app/transitionWorkflow';
import type { copyAppQuery, copyAppResponse } from '@/pages/api/core/app/copy';
/* folder */ /* folder */
export const postCreateAppFolder = (data: CreateAppFolderBody) => export const postCreateAppFolder = (data: CreateAppFolderBody) =>
@ -15,6 +16,7 @@ export const getAppFolderPath = (parentId: ParentIdType) =>
GET<ParentTreePathItemType[]>(`/core/app/folder/path`, { parentId }); GET<ParentTreePathItemType[]>(`/core/app/folder/path`, { parentId });
/* detail */ /* detail */
export const postTransition2Workflow = (data: transitionWorkflowBody) => export const postTransition2Workflow = (data: transitionWorkflowBody) =>
POST<transitionWorkflowResponse>('/core/app/transitionWorkflow', data); POST<transitionWorkflowResponse>('/core/app/transitionWorkflow', data);
export const postCopyApp = (data: copyAppQuery) => POST<copyAppResponse>('/core/app/copy', data);

View File

@ -35,7 +35,7 @@
"@nestjs/schematics": "^10.0.0", "@nestjs/schematics": "^10.0.0",
"@nestjs/testing": "^10.0.0", "@nestjs/testing": "^10.0.0",
"@types/jest": "^29.5.2", "@types/jest": "^29.5.2",
"@types/node": "^20.3.1", "@types/node": "^20.14.2",
"@types/supertest": "^6.0.0", "@types/supertest": "^6.0.0",
"@typescript-eslint/eslint-plugin": "^6.0.0", "@typescript-eslint/eslint-plugin": "^6.0.0",
"@typescript-eslint/parser": "^6.0.0", "@typescript-eslint/parser": "^6.0.0",