mirror of
https://github.com/labring/FastGPT.git
synced 2025-07-23 13:03:50 +00:00
perf: forbid image to base64 (#3038)
* perf: forbid image to base64 * update file upload path * feat: support promptCall use image * fix: echarts load * update doc
This commit is contained in:
14
docSite/content/zh-cn/docs/development/upgrading/4813.md
Normal file
14
docSite/content/zh-cn/docs/development/upgrading/4813.md
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
---
|
||||||
|
title: 'V4.8.13(进行中)'
|
||||||
|
description: 'FastGPT V4.8.13 更新说明'
|
||||||
|
icon: 'upgrade'
|
||||||
|
draft: false
|
||||||
|
toc: true
|
||||||
|
weight: 811
|
||||||
|
---
|
||||||
|
|
||||||
|
## 更新说明
|
||||||
|
|
||||||
|
1.
|
||||||
|
2. 优化 - 知识库上传文件,优化报错提示
|
||||||
|
3. 修复 - BI 图表生成无法写入文件
|
@@ -1,7 +1,5 @@
|
|||||||
import * as echarts from 'echarts';
|
import * as echarts from 'echarts';
|
||||||
import json5 from 'json5';
|
import json5 from 'json5';
|
||||||
import { getFileSavePath } from '../../../utils';
|
|
||||||
import * as fs from 'fs';
|
|
||||||
import { SystemPluginSpecialResponse } from '../../../type.d';
|
import { SystemPluginSpecialResponse } from '../../../type.d';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@@ -82,25 +80,23 @@ const generateChart = async (title: string, xAxis: string, yAxis: string, chartT
|
|||||||
|
|
||||||
chart.setOption(option);
|
chart.setOption(option);
|
||||||
// 生成 Base64 图像
|
// 生成 Base64 图像
|
||||||
const base64Image = chart.getDataURL();
|
const base64Image = chart.getDataURL({
|
||||||
const svgData = decodeURIComponent(base64Image.split(',')[1]);
|
type: 'png',
|
||||||
|
pixelRatio: 2 // 可以设置更高的像素比以获得更清晰的图像
|
||||||
|
});
|
||||||
|
const svgContent = decodeURIComponent(base64Image.split(',')[1]);
|
||||||
|
const base64 = `data:image/svg+xml;base64,${Buffer.from(svgContent).toString('base64')}`;
|
||||||
|
|
||||||
const fileName = `chart_${Date.now()}.svg`;
|
return base64;
|
||||||
const filePath = getFileSavePath(fileName);
|
|
||||||
fs.writeFileSync(filePath, svgData);
|
|
||||||
// 释放图表实例
|
|
||||||
chart.dispose();
|
|
||||||
|
|
||||||
return filePath;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const main = async ({ title, xAxis, yAxis, chartType }: Props): Response => {
|
const main = async ({ title, xAxis, yAxis, chartType }: Props): Response => {
|
||||||
const filePath = await generateChart(title, xAxis, yAxis, chartType);
|
const base64 = await generateChart(title, xAxis, yAxis, chartType);
|
||||||
return {
|
return {
|
||||||
result: {
|
result: {
|
||||||
type: 'SYSTEM_PLUGIN_FILE',
|
type: 'SYSTEM_PLUGIN_BASE64',
|
||||||
path: filePath,
|
value: base64,
|
||||||
contentType: 'image/svg+xml'
|
extension: 'svg'
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
6
packages/plugins/type.d.ts
vendored
6
packages/plugins/type.d.ts
vendored
@@ -4,9 +4,9 @@ import { SystemPluginTemplateItemType } from '@fastgpt/global/core/workflow/type
|
|||||||
|
|
||||||
export type SystemPluginResponseType = Promise<Record<string, any>>;
|
export type SystemPluginResponseType = Promise<Record<string, any>>;
|
||||||
export type SystemPluginSpecialResponse = {
|
export type SystemPluginSpecialResponse = {
|
||||||
type: 'SYSTEM_PLUGIN_FILE';
|
type: 'SYSTEM_PLUGIN_BASE64';
|
||||||
path: string;
|
value: string;
|
||||||
contentType: string;
|
extension: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
|
@@ -1,15 +0,0 @@
|
|||||||
import path from 'path';
|
|
||||||
import * as fs from 'fs';
|
|
||||||
|
|
||||||
const isProduction = process.env.NODE_ENV === 'production';
|
|
||||||
|
|
||||||
export const getFileSavePath = (name: string) => {
|
|
||||||
if (isProduction) {
|
|
||||||
return `/app/plugin_file/${name}`;
|
|
||||||
}
|
|
||||||
const filePath = path.join(process.cwd(), 'local', 'plugin_file', name);
|
|
||||||
|
|
||||||
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
||||||
|
|
||||||
return filePath;
|
|
||||||
};
|
|
@@ -12,6 +12,7 @@ import { gridFsStream2Buffer, stream2Encoding } from './utils';
|
|||||||
import { addLog } from '../../system/log';
|
import { addLog } from '../../system/log';
|
||||||
import { readFromSecondary } from '../../mongo/utils';
|
import { readFromSecondary } from '../../mongo/utils';
|
||||||
import { parseFileExtensionFromUrl } from '@fastgpt/global/common/string/tools';
|
import { parseFileExtensionFromUrl } from '@fastgpt/global/common/string/tools';
|
||||||
|
import { Readable } from 'stream';
|
||||||
|
|
||||||
export function getGFSCollection(bucket: `${BucketNameEnum}`) {
|
export function getGFSCollection(bucket: `${BucketNameEnum}`) {
|
||||||
MongoDatasetFileSchema;
|
MongoDatasetFileSchema;
|
||||||
@@ -76,6 +77,59 @@ export async function uploadFile({
|
|||||||
|
|
||||||
return String(stream.id);
|
return String(stream.id);
|
||||||
}
|
}
|
||||||
|
export async function uploadFileFromBase64Img({
|
||||||
|
bucketName,
|
||||||
|
teamId,
|
||||||
|
tmbId,
|
||||||
|
base64,
|
||||||
|
filename,
|
||||||
|
metadata = {}
|
||||||
|
}: {
|
||||||
|
bucketName: `${BucketNameEnum}`;
|
||||||
|
teamId: string;
|
||||||
|
tmbId: string;
|
||||||
|
base64: string;
|
||||||
|
filename: string;
|
||||||
|
metadata?: Record<string, any>;
|
||||||
|
}) {
|
||||||
|
if (!base64) return Promise.reject(`filePath is empty`);
|
||||||
|
if (!filename) return Promise.reject(`filename is empty`);
|
||||||
|
|
||||||
|
const base64Data = base64.split(',')[1];
|
||||||
|
const contentType = base64.split(',')?.[0]?.split?.(':')?.[1];
|
||||||
|
const buffer = Buffer.from(base64Data, 'base64');
|
||||||
|
const readableStream = new Readable({
|
||||||
|
read() {
|
||||||
|
this.push(buffer);
|
||||||
|
this.push(null);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const { stream: readStream, encoding } = await stream2Encoding(readableStream);
|
||||||
|
|
||||||
|
// Add default metadata
|
||||||
|
metadata.teamId = teamId;
|
||||||
|
metadata.tmbId = tmbId;
|
||||||
|
metadata.encoding = encoding;
|
||||||
|
|
||||||
|
// create a gridfs bucket
|
||||||
|
const bucket = getGridBucket(bucketName);
|
||||||
|
|
||||||
|
const stream = bucket.openUploadStream(filename, {
|
||||||
|
metadata,
|
||||||
|
contentType
|
||||||
|
});
|
||||||
|
|
||||||
|
// save to gridfs
|
||||||
|
await new Promise((resolve, reject) => {
|
||||||
|
readStream
|
||||||
|
.pipe(stream as any)
|
||||||
|
.on('finish', resolve)
|
||||||
|
.on('error', reject);
|
||||||
|
});
|
||||||
|
|
||||||
|
return String(stream.id);
|
||||||
|
}
|
||||||
|
|
||||||
export async function getFileById({
|
export async function getFileById({
|
||||||
bucketName,
|
bucketName,
|
||||||
|
@@ -104,6 +104,9 @@ export const loadRequestMessages = async ({
|
|||||||
}) => {
|
}) => {
|
||||||
// Load image to base64
|
// Load image to base64
|
||||||
const loadImageToBase64 = async (messages: ChatCompletionContentPart[]) => {
|
const loadImageToBase64 = async (messages: ChatCompletionContentPart[]) => {
|
||||||
|
if (process.env.MULTIPLE_DATA_TO_BASE64 === 'false') {
|
||||||
|
return messages;
|
||||||
|
}
|
||||||
return Promise.all(
|
return Promise.all(
|
||||||
messages.map(async (item) => {
|
messages.map(async (item) => {
|
||||||
if (item.type === 'image_url') {
|
if (item.type === 'image_url') {
|
||||||
|
@@ -185,7 +185,7 @@ export const dispatchRunTools = async (props: DispatchToolModuleProps): Promise<
|
|||||||
// array, replace last element
|
// array, replace last element
|
||||||
const lastText = lastMessage.content[lastMessage.content.length - 1];
|
const lastText = lastMessage.content[lastMessage.content.length - 1];
|
||||||
if (lastText.type === 'text') {
|
if (lastText.type === 'text') {
|
||||||
lastMessage.content = replaceVariable(Prompt_Tool_Call, {
|
lastText.text = replaceVariable(Prompt_Tool_Call, {
|
||||||
question: lastText.text
|
question: lastText.text
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
@@ -176,17 +176,29 @@ export const runToolWithPromptCall = async (
|
|||||||
);
|
);
|
||||||
|
|
||||||
const lastMessage = messages[messages.length - 1];
|
const lastMessage = messages[messages.length - 1];
|
||||||
if (typeof lastMessage.content !== 'string') {
|
if (typeof lastMessage.content === 'string') {
|
||||||
|
lastMessage.content = replaceVariable(lastMessage.content, {
|
||||||
|
toolsPrompt
|
||||||
|
});
|
||||||
|
} else if (Array.isArray(lastMessage.content)) {
|
||||||
|
// array, replace last element
|
||||||
|
const lastText = lastMessage.content[lastMessage.content.length - 1];
|
||||||
|
if (lastText.type === 'text') {
|
||||||
|
lastText.text = replaceVariable(lastText.text, {
|
||||||
|
toolsPrompt
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
return Promise.reject('Prompt call invalid input');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
return Promise.reject('Prompt call invalid input');
|
return Promise.reject('Prompt call invalid input');
|
||||||
}
|
}
|
||||||
lastMessage.content = replaceVariable(lastMessage.content, {
|
|
||||||
toolsPrompt
|
|
||||||
});
|
|
||||||
|
|
||||||
const filterMessages = await filterGPTMessageByMaxTokens({
|
const filterMessages = await filterGPTMessageByMaxTokens({
|
||||||
messages,
|
messages,
|
||||||
maxTokens: toolModel.maxContext - 500 // filter token. not response maxToken
|
maxTokens: toolModel.maxContext - 500 // filter token. not response maxToken
|
||||||
});
|
});
|
||||||
|
|
||||||
const [requestMessages, max_tokens] = await Promise.all([
|
const [requestMessages, max_tokens] = await Promise.all([
|
||||||
loadRequestMessages({
|
loadRequestMessages({
|
||||||
messages: filterMessages,
|
messages: filterMessages,
|
||||||
@@ -398,11 +410,27 @@ export const runToolWithPromptCall = async (
|
|||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
// get the next user prompt
|
// get the next user prompt
|
||||||
lastMessage.content += `${replaceAnswer}
|
if (typeof lastMessage.content === 'string') {
|
||||||
|
lastMessage.content += `${replaceAnswer}
|
||||||
TOOL_RESPONSE: """
|
TOOL_RESPONSE: """
|
||||||
${workflowInteractiveResponseItem ? `{{${INTERACTIVE_STOP_SIGNAL}}}` : toolsRunResponse.toolResponsePrompt}
|
${workflowInteractiveResponseItem ? `{{${INTERACTIVE_STOP_SIGNAL}}}` : toolsRunResponse.toolResponsePrompt}
|
||||||
"""
|
"""
|
||||||
ANSWER: `;
|
ANSWER: `;
|
||||||
|
} else if (Array.isArray(lastMessage.content)) {
|
||||||
|
// array, replace last element
|
||||||
|
const lastText = lastMessage.content[lastMessage.content.length - 1];
|
||||||
|
if (lastText.type === 'text') {
|
||||||
|
lastText.text += `${replaceAnswer}
|
||||||
|
TOOL_RESPONSE: """
|
||||||
|
${workflowInteractiveResponseItem ? `{{${INTERACTIVE_STOP_SIGNAL}}}` : toolsRunResponse.toolResponsePrompt}
|
||||||
|
"""
|
||||||
|
ANSWER: `;
|
||||||
|
} else {
|
||||||
|
return Promise.reject('Prompt call invalid input');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return Promise.reject('Prompt call invalid input');
|
||||||
|
}
|
||||||
|
|
||||||
const runTimes = (response?.runTimes || 0) + toolsRunResponse.toolResponse.runTimes;
|
const runTimes = (response?.runTimes || 0) + toolsRunResponse.toolResponse.runTimes;
|
||||||
const toolNodeTokens = response?.toolNodeTokens ? response.toolNodeTokens + tokens : tokens;
|
const toolNodeTokens = response?.toolNodeTokens ? response.toolNodeTokens + tokens : tokens;
|
||||||
|
@@ -18,11 +18,11 @@ import { textAdaptGptResponse } from '@fastgpt/global/core/workflow/runtime/util
|
|||||||
import { getSystemPluginCb } from '../../../../../plugins/register';
|
import { getSystemPluginCb } from '../../../../../plugins/register';
|
||||||
import { ContentTypes } from '@fastgpt/global/core/workflow/constants';
|
import { ContentTypes } from '@fastgpt/global/core/workflow/constants';
|
||||||
import { replaceEditorVariable } from '@fastgpt/global/core/workflow/utils';
|
import { replaceEditorVariable } from '@fastgpt/global/core/workflow/utils';
|
||||||
import { uploadFile } from '../../../../common/file/gridfs/controller';
|
import { uploadFileFromBase64Img } from '../../../../common/file/gridfs/controller';
|
||||||
import { ReadFileBaseUrl } from '@fastgpt/global/common/file/constants';
|
import { ReadFileBaseUrl } from '@fastgpt/global/common/file/constants';
|
||||||
import { createFileToken } from '../../../../support/permission/controller';
|
import { createFileToken } from '../../../../support/permission/controller';
|
||||||
import { removeFilesByPaths } from '../../../../common/file/utils';
|
|
||||||
import { JSONPath } from 'jsonpath-plus';
|
import { JSONPath } from 'jsonpath-plus';
|
||||||
|
import type { SystemPluginSpecialResponse } from '../../../../../plugins/type';
|
||||||
|
|
||||||
type PropsArrType = {
|
type PropsArrType = {
|
||||||
key: string;
|
key: string;
|
||||||
@@ -376,27 +376,25 @@ async function replaceSystemPluginResponse({
|
|||||||
tmbId: string;
|
tmbId: string;
|
||||||
}) {
|
}) {
|
||||||
for await (const key of Object.keys(response)) {
|
for await (const key of Object.keys(response)) {
|
||||||
if (typeof response[key] === 'object' && response[key].type === 'SYSTEM_PLUGIN_FILE') {
|
if (typeof response[key] === 'object' && response[key].type === 'SYSTEM_PLUGIN_BASE64') {
|
||||||
const fileObj = response[key];
|
const fileObj = response[key] as SystemPluginSpecialResponse;
|
||||||
const filename = fileObj.path.split('/').pop() || `${tmbId}-${Date.now()}`;
|
const filename = `${tmbId}-${Date.now()}.${fileObj.extension}`;
|
||||||
try {
|
try {
|
||||||
const fileId = await uploadFile({
|
const fileId = await uploadFileFromBase64Img({
|
||||||
teamId,
|
teamId,
|
||||||
tmbId,
|
tmbId,
|
||||||
bucketName: 'chat',
|
bucketName: 'chat',
|
||||||
path: fileObj.path,
|
base64: fileObj.value,
|
||||||
filename,
|
filename,
|
||||||
contentType: fileObj.contentType,
|
|
||||||
metadata: {}
|
metadata: {}
|
||||||
});
|
});
|
||||||
response[key] = `${ReadFileBaseUrl}?filename=${filename}&token=${await createFileToken({
|
response[key] = `${ReadFileBaseUrl}/${filename}?token=${await createFileToken({
|
||||||
bucketName: 'chat',
|
bucketName: 'chat',
|
||||||
teamId,
|
teamId,
|
||||||
tmbId,
|
tmbId,
|
||||||
fileId
|
fileId
|
||||||
})}`;
|
})}`;
|
||||||
} catch (error) {}
|
} catch (error) {}
|
||||||
removeFilesByPaths([fileObj.path]);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return response;
|
return response;
|
||||||
|
@@ -70,9 +70,9 @@ COPY --from=maindeps /app/node_modules/@zilliz/milvus2-sdk-node ./node_modules/@
|
|||||||
|
|
||||||
# copy package.json to version file
|
# copy package.json to version file
|
||||||
COPY --from=builder /app/projects/app/package.json ./package.json
|
COPY --from=builder /app/projects/app/package.json ./package.json
|
||||||
|
|
||||||
# copy config
|
# copy config
|
||||||
COPY ./projects/app/data /app/data
|
COPY ./projects/app/data /app/data
|
||||||
|
|
||||||
RUN chown -R nextjs:nodejs /app/data
|
RUN chown -R nextjs:nodejs /app/data
|
||||||
|
|
||||||
ENV NODE_ENV=production
|
ENV NODE_ENV=production
|
||||||
|
63
projects/app/src/pages/api/common/file/read/[filename].ts
Normal file
63
projects/app/src/pages/api/common/file/read/[filename].ts
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||||
|
import { jsonRes } from '@fastgpt/service/common/response';
|
||||||
|
import { connectToDatabase } from '@/service/mongo';
|
||||||
|
import { authFileToken } from '@fastgpt/service/support/permission/controller';
|
||||||
|
import { getDownloadStream, getFileById } from '@fastgpt/service/common/file/gridfs/controller';
|
||||||
|
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>) {
|
||||||
|
try {
|
||||||
|
await connectToDatabase();
|
||||||
|
|
||||||
|
const { token, filename } = req.query as { token: string; filename: string };
|
||||||
|
|
||||||
|
const { fileId, bucketName } = await authFileToken(token);
|
||||||
|
|
||||||
|
if (!fileId) {
|
||||||
|
throw new Error('fileId is empty');
|
||||||
|
}
|
||||||
|
|
||||||
|
const [file, fileStream] = await Promise.all([
|
||||||
|
getFileById({ bucketName, fileId }),
|
||||||
|
getDownloadStream({ bucketName, fileId })
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (!file) {
|
||||||
|
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('Cache-Control', 'public, max-age=31536000');
|
||||||
|
res.setHeader('Content-Disposition', `inline; filename="${encodeURIComponent(filename)}"`);
|
||||||
|
|
||||||
|
stream.pipe(res);
|
||||||
|
|
||||||
|
stream.on('error', () => {
|
||||||
|
res.status(500).end();
|
||||||
|
});
|
||||||
|
stream.on('end', () => {
|
||||||
|
res.end();
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
jsonRes(res, {
|
||||||
|
code: 500,
|
||||||
|
error
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export const config = {
|
||||||
|
api: {
|
||||||
|
responseLimit: '100mb'
|
||||||
|
}
|
||||||
|
};
|
@@ -57,14 +57,12 @@ async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
|
|||||||
jsonRes(res, {
|
jsonRes(res, {
|
||||||
data: {
|
data: {
|
||||||
fileId,
|
fileId,
|
||||||
previewUrl: `${ReadFileBaseUrl}?filename=${file.originalname}&token=${await createFileToken(
|
previewUrl: `${ReadFileBaseUrl}/${file.originalname}?token=${await createFileToken({
|
||||||
{
|
bucketName,
|
||||||
bucketName,
|
teamId,
|
||||||
teamId,
|
tmbId,
|
||||||
tmbId,
|
fileId
|
||||||
fileId
|
})}`
|
||||||
}
|
|
||||||
)}`
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
Reference in New Issue
Block a user